diff frontends/src/quick_frontend/quick_app.py @ 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents ba87b940f07a
children 0befb14ecf62
line wrap: on
line diff
--- a/frontends/src/quick_frontend/quick_app.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/frontends/src/quick_frontend/quick_app.py	Wed Mar 18 10:52:28 2015 +0100
@@ -17,40 +17,228 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from sat.core.i18n import _
-import sys
 from sat.core.log import getLogger
 log = getLogger(__name__)
-from sat_frontends.tools.jid import JID
-from sat_frontends.bridge.DBus import DBusBridgeFrontend
+
+from sat.core.i18n import _
 from sat.core import exceptions
-from sat_frontends.quick_frontend.quick_utils import escapePrivate, unescapePrivate
-from optparse import OptionParser
+from sat.tools.misc import TriggerManager
+
+from sat_frontends.tools import jid
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_menus
+from sat_frontends.quick_frontend import quick_chat, quick_games
+from sat_frontends.quick_frontend.constants import Const as C
+
+import sys
+from collections import OrderedDict
+
+try:
+    # FIXME: to be removed when an acceptable solution is here
+    unicode('')  # XXX: unicode doesn't exist in pyjamas
+except (TypeError, AttributeError):  # Error raised is not the same depending on pyjsbuild options
+    unicode = str
+
+
+class ProfileManager(object):
+    """Class managing all data relative to one profile, and plugging in mechanism"""
+    host = None
+    bridge = None
+    cache_keys_to_get = ['avatar']
+
+    def __init__(self, profile):
+        self.profile = profile
+        self.whoami = None
+        self.data = {}
+
+    def __getitem__(self, key):
+        return self.data[key]
+
+    def __setitem__(self, key, value):
+        self.data[key] = value
+
+    def plug(self):
+        """Plug the profile to the host"""
+        # we get the essential params
+        self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile,
+                                   callback=self._plug_profile_jid, errback=self._getParamError)
+
+    def _plug_profile_jid(self, _jid):
+        self.whoami = jid.JID(_jid)
+        self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=self.profile,
+                                   callback=self._plug_profile_autoconnect, errback=self._getParamError)
+
+    def _plug_profile_autoconnect(self, value_str):
+        autoconnect = C.bool(value_str)
+        if autoconnect and not self.bridge.isConnected(self.profile):
+            self.host.asyncConnect(self.profile, callback=lambda dummy: self._plug_profile_afterconnect())
+        else:
+            self._plug_profile_afterconnect()
+
+    def _plug_profile_afterconnect(self):
+        # Profile can be connected or not
+        # we get cached data
+        self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues)
+
+    def _plug_profile_failedCachedValues(self, failure):
+        log.error("Couldn't get cached values: {}".format(failure))
+        self._plug_profile_gotCachedValues({})
+
+    def _plug_profile_gotCachedValues(self, cached_values):
+        # TODO: watched plugin
+
+        # add the contact list and its listener
+        contact_list = self.host.addContactList(self.profile)
+        self.host.contact_lists[self.profile] = contact_list
+
+        for entity, data in cached_values.iteritems():
+            for key, value in data.iteritems():
+                contact_list.setCache(jid.JID(entity), key, value)
+
+        if not self.bridge.isConnected(self.profile):
+            self.host.setStatusOnline(False, profile=self.profile)
+        else:
+            self.host.setStatusOnline(True, profile=self.profile)
+
+            contact_list.fill()
+
+            #The waiting subscription requests
+            self.bridge.getWaitingSub(self.profile, callback=self._plug_profile_gotWaitingSub)
 
-from sat_frontends.quick_frontend.constants import Const as C
+    def _plug_profile_gotWaitingSub(self, waiting_sub):
+        for sub in waiting_sub:
+            self.host.subscribeHandler(waiting_sub[sub], sub, self.profile)
+
+        self.bridge.getRoomsJoined(self.profile, callback=self._plug_profile_gotRoomsJoined)
+
+    def _plug_profile_gotRoomsJoined(self, rooms_args):
+        #Now we open the MUC window where we already are:
+        for room_args in rooms_args:
+            self.host.roomJoinedHandler(*room_args, profile=self.profile)
+
+        self.bridge.getRoomsSubjects(self.profile, callback=self._plug_profile_gotRoomsSubjects)
+
+    def _plug_profile_gotRoomsSubjects(self, subjects_args):
+        for subject_args in subjects_args:
+            self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
+
+        #Presence must be requested after rooms are filled
+        self.host.bridge.getPresenceStatuses(self.profile, callback=self._plug_profile_gotPresences)
+
+
+    def _plug_profile_gotPresences(self, presences):
+        def gotEntityData(data, contact):
+            for key in ('avatar', 'nick'):
+                if key in data:
+                    self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile)
+
+        for contact in presences:
+            for res in presences[contact]:
+                jabber_id = ('%s/%s' % (jid.JID(contact).bare, res)) if res else contact
+                show = presences[contact][res][0]
+                priority = presences[contact][res][1]
+                statuses = presences[contact][res][2]
+                self.host.presenceUpdateHandler(jabber_id, show, priority, statuses, self.profile)
+            self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile, callback=lambda data, contact=contact: gotEntityData(data, contact), errback=lambda failure, contact=contact: log.debug("No cache data for {}".format(contact)))
+
+        #Finaly, we get the waiting confirmation requests
+        self.bridge.getWaitingConf(self.profile, callback=self._plug_profile_gotWaitingConf)
+
+    def _plug_profile_gotWaitingConf(self, waiting_confs):
+        for confirm_id, confirm_type, data in waiting_confs:
+            self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile)
+
+        # At this point, profile should be fully plugged
+        # and we launch frontend specific method
+        self.host.profilePlugged(self.profile)
+
+    def _getParamError(self, ignore):
+        log.error(_("Can't get profile parameter"))
+
+
+class ProfilesManager(object):
+    """Class managing collection of profiles"""
+
+    def __init__(self):
+        self._profiles = {}
+
+    def __contains__(self, profile):
+        return profile in self._profiles
+
+    def __iter__(self):
+        return self._profiles.iterkeys()
+
+    def __getitem__(self, profile):
+        return self._profiles[profile]
+
+    def __len__(self):
+        return len(self._profiles)
+
+    def plug(self, profile):
+        if profile in self._profiles:
+            raise exceptions.ConflictError('A profile of the name [{}] is already plugged'.format(profile))
+        self._profiles[profile] = ProfileManager(profile)
+        self._profiles[profile].plug()
+
+    def unplug(self, profile):
+        if profile not in self._profiles:
+            raise ValueError('The profile [{}] is not plugged'.format(profile))
+
+        # remove the contact list and its listener
+        host = self._profiles[profile].host
+        host.contact_lists[profile].onDelete()
+        del host.contact_lists[profile]
+
+        del self._profiles[profile]
+
+    def chooseOneProfile(self):
+        return self._profiles.keys()[0]
 
 
 class QuickApp(object):
     """This class contain the main methods needed for the frontend"""
 
-    def __init__(self, single_profile=True):
-        self.profiles = {}
-        self.single_profile = single_profile
-        self.check_options()
+    def __init__(self, create_bridge, check_options=None):
+        """Create a frontend application
+
+        @param create_bridge: method to use to create the Bridge
+        @param check_options: method to call to check options (usually command line arguments)
+        """
+        self.menus = quick_menus.QuickMenusManager(self)
+        ProfileManager.host = self
+        self.profiles = ProfilesManager()
+        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.contact_lists = {}
+        self.widgets = quick_widgets.QuickWidgetsManager(self)
+        if check_options is not None:
+            self.options = check_options()
+        else:
+            self.options = None
+
+        # widgets
+        self.selected_widget = None # widget currently selected (must be filled by frontend)
+
+        # listeners
+        self._listeners = {} # key: listener type ("avatar", "selected", etc), value: list of callbacks
+
+        # triggers
+        self.trigger = TriggerManager()  # trigger are used to change the default behaviour
 
         ## bridge ##
         try:
-            self.bridge = DBusBridgeFrontend()
+            self.bridge = create_bridge()
         except exceptions.BridgeExceptionNoService:
             print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
             sys.exit(1)
         except exceptions.BridgeInitError:
             print(_(u"Can't init bridge"))
             sys.exit(1)
+        ProfileManager.bridge = self.bridge
         self.registerSignal("connected")
         self.registerSignal("disconnected")
         self.registerSignal("newContact")
-        self.registerSignal("newMessage", self._newMessage)
+        self.registerSignal("newMessage")
         self.registerSignal("newAlert")
         self.registerSignal("presenceUpdate")
         self.registerSignal("subscribe")
@@ -66,40 +254,43 @@
         self.registerSignal("roomUserLeft", iface="plugin")
         self.registerSignal("roomUserChangedNick", iface="plugin")
         self.registerSignal("roomNewSubject", iface="plugin")
-        self.registerSignal("tarotGameStarted", iface="plugin")
-        self.registerSignal("tarotGameNew", iface="plugin")
-        self.registerSignal("tarotGameChooseContrat", iface="plugin")
-        self.registerSignal("tarotGameShowCards", iface="plugin")
-        self.registerSignal("tarotGameYourTurn", iface="plugin")
-        self.registerSignal("tarotGameScore", iface="plugin")
-        self.registerSignal("tarotGameCardsPlayed", iface="plugin")
-        self.registerSignal("tarotGameInvalidCards", iface="plugin")
-        self.registerSignal("quizGameStarted", iface="plugin")
-        self.registerSignal("quizGameNew", iface="plugin")
-        self.registerSignal("quizGameQuestion", iface="plugin")
-        self.registerSignal("quizGamePlayerBuzzed", iface="plugin")
-        self.registerSignal("quizGamePlayerSays", iface="plugin")
-        self.registerSignal("quizGameAnswerResult", iface="plugin")
-        self.registerSignal("quizGameTimerExpired", iface="plugin")
-        self.registerSignal("quizGameTimerRestarted", iface="plugin")
         self.registerSignal("chatStateReceived", iface="plugin")
+        self.registerSignal("personalEvent", iface="plugin")
 
-        self.current_action_ids = set()
-        self.current_action_ids_cb = {}
+        # FIXME: do it dynamically
+        quick_games.Tarot.registerSignals(self)
+        quick_games.Quiz.registerSignals(self)
+        quick_games.Radiocol.registerSignals(self)
+
+        self.current_action_ids = set() # FIXME: to be removed
+        self.current_action_ids_cb = {} # FIXME: to be removed
         self.media_dir = self.bridge.getConfig('', 'media_dir')
 
-    def registerSignal(self, functionName, handler=None, iface="core", with_profile=True):
+    @property
+    def current_profile(self):
+        """Profile that a user would expect to use"""
+        try:
+            return self.selected_widget.profile
+        except (TypeError, AttributeError):
+            return self.profiles.chooseOneProfile()
+
+    @property
+    def visible_widgets(self):
+        """widgets currently visible (must be implemented by frontend)"""
+        raise NotImplementedError
+
+    def registerSignal(self, function_name, handler=None, iface="core", with_profile=True):
         """Register a handler for a signal
 
-        @param functionName (str): name of the signal to handle
-        @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (functionName + 'Handler')
+        @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 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
         """
         if handler is None:
-            handler = getattr(self, "%s%s" % (functionName, 'Handler'))
+            handler = getattr(self, "{}{}".format(function_name, 'Handler'))
         if not with_profile:
-            self.bridge.register(functionName, handler, iface)
+            self.bridge.register(function_name, handler, iface)
             return
 
         def signalReceived(*args, **kwargs):
@@ -108,78 +299,97 @@
                 if not args:
                     raise exceptions.ProfileNotSetError
                 profile = args[-1]
-            if profile is not None and not self.check_profile(profile):
-                return  # we ignore signal for profiles we don't manage
+            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
+                        self.signals_cache.setdefault(profile, []).append((function_name, handler, args, kwargs))
+                    return  # we ignore signal for profiles we don't manage
             handler(*args, **kwargs)
-        self.bridge.register(functionName, signalReceived, iface)
+        self.bridge.register(function_name, signalReceived, iface)
+
+    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)
+        @param type_: type of event, can be:
+            - avatar: called when avatar data is updated
+                args: (entity, avatar file, profile)
+            - nick: called when nick data is updated
+                args: (entity, new_nick, profile)
+            - presence: called when a presence is received
+                args: (entity, show, priority, statuses, profile)
+            - menu: called when a menu item is added or removed
+                args: (type_, path, path_i18n, item) were values are:
+                    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
+            - gotMenus: called only once when menu are available (no arg)
+        @param callback: method to call on event
+        @param profiles_filter (set[unicode]): if set and not empty, the
+            listener will be callable only by one of the given profiles.
+        """
+        assert type_ in C.LISTENERS
+        self._listeners.setdefault(type_, OrderedDict())[callback] = profiles_filter
+
+    def removeListener(self, type_, callback):
+        """Remove a callback from listeners
+
+        @param type_: same as for [addListener]
+        @param callback: callback to remove
+        """
+        assert type_ in C.LISTENERS
+        self._listeners[type_].pop(callback)
+
+    def callListeners(self, type_, *args, **kwargs):
+        """Call the methods which listen type_ event. If a profiles filter has
+        been register with a listener and profile argument is not None, the
+        listener will be called only if profile is in the profiles filter list.
+
+        @param type_: same as for [addListener]
+        @param *args: arguments sent to callback
+        @param **kwargs: keywords argument, mainly used to pass "profile" when needed
+        """
+        assert type_ in C.LISTENERS
+        try:
+            listeners = self._listeners[type_]
+        except KeyError:
+            pass
+        else:
+            profile = kwargs.get("profile")
+            for listener, profiles_filter in listeners.iteritems():
+                if profile is None or not profiles_filter or profile in profiles_filter:
+                    listener(*args, **kwargs)
 
     def check_profile(self, profile):
-        """Tell if the profile is currently followed by the application"""
-        return profile in self.profiles.keys()
+        """Tell if the profile is currently followed by the application, and ready"""
+        return profile in self.ready_profiles
 
-    def postInit(self):
-        """Must be called after initialization is done, do all automatic task (auto plug profile)"""
-        if self.options.profile:
-            if not self.bridge.getProfileName(self.options.profile):
-                log.error(_("Trying to plug an unknown profile (%s)" % self.options.profile))
-            else:
-                self.plug_profile(self.options.profile)
+    def postInit(self, profile_manager):
+        """Must be called after initialization is done, do all automatic task (auto plug profile)
 
-    def check_options(self):
-        """Check command line options"""
-        usage = _("""
-        %prog [options]
-
-        %prog --help for options list
-        """)
-        parser = OptionParser(usage=usage)
+        @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
+        """
+        if self.options and self.options.profile:
+            profile_manager.autoconnect([self.options.profile])
 
-        parser.add_option("-p", "--profile", help=_("Select the profile to use"))
-
-        (self.options, args) = parser.parse_args()
-        if self.options.profile:
-            self.options.profile = self.options.profile.decode('utf-8')
-        return args
-
-    def _getParamError(self, ignore):
-        log.error(_("Can't get profile parameter"))
+    def profilePlugged(self, profile):
+        """Method called when the profile is fully plugged, to launch frontend specific workflow
 
-    def plug_profile(self, profile_key='@DEFAULT@'):
-        """Tell application which profile must be used"""
-        if self.single_profile and self.profiles:
-            log.error(_('There is already one profile plugged (we are in single profile mode) !'))
-            return
-        profile = self.bridge.getProfileName(profile_key)
-        if not profile:
-            log.error(_("The profile asked doesn't exist"))
-            return
-        if profile in self.profiles:
-            log.warning(_("The profile is already plugged"))
-            return
-        self.profiles[profile] = {}
-        if self.single_profile:
-            self.profile = profile # FIXME: must be refactored (multi profiles are not managed correclty)
-        raw_menus = self.bridge.getMenus("", C.NO_SECURITY_LIMIT )
-        menus = self.profiles[profile]['menus'] = {}
-        for raw_menu in raw_menus:
-            id_, type_, path, path_i18n  = raw_menu
-            menus_data = menus.setdefault(type_, [])
-            menus_data.append((id_, path, path_i18n))
-        self.launchAction(C.AUTHENTICATE_PROFILE_ID, {'caller': 'plug_profile'}, profile_key=profile)
+        /!\ 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
+        """
+        self.ready_profiles.add(profile)
 
-    def plug_profile_1(self, profile):
-        ###now we get the essential params###
-        self.bridge.asyncGetParamA("JabberID", "Connection", profile_key=profile,
-                                   callback=lambda _jid: self.plug_profile_2(_jid, profile), errback=self._getParamError)
+        # profile is ready, we can call send signals that where is cache
+        cached_signals = self.signals_cache.pop(profile, [])
+        for function_name, handler, args, kwargs in cached_signals:
+            log.debug(u"Calling cached signal [%s] with args %s and kwargs %s" % (function_name, args, kwargs))
 
-    def plug_profile_2(self, _jid, profile):
-        self.profiles[profile]['whoami'] = JID(_jid)
-        self.bridge.asyncGetParamA("autoconnect", "Connection", profile_key=profile,
-                                   callback=lambda value: self.plug_profile_3(value == "true", profile), errback=self._getParamError)
-
-    def plug_profile_3(self, autoconnect, profile):
-        self.bridge.asyncGetParamA("Watched", "Misc", profile_key=profile,
-                                   callback=lambda watched: self.plug_profile_4(watched, autoconnect, profile), errback=self._getParamError)
+        self.callListeners('profilePlugged', profile=profile)
 
     def asyncConnect(self, profile, callback=None, errback=None):
         if not callback:
@@ -188,351 +398,215 @@
             def errback(failure):
                 log.error(_(u"Can't connect profile [%s]") % failure)
                 if failure.module.startswith('twisted.words.protocols.jabber') and failure.condition == "not-authorized":
-                    self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile_key=profile)
+                    self.launchAction(C.CHANGE_XMPP_PASSWD_ID, {}, profile=profile)
                 else:
                     self.showDialog(failure.message, failure.fullname, 'error')
         self.bridge.asyncConnect(profile, callback=callback, errback=errback)
 
-    def plug_profile_4(self, watched, autoconnect, profile):
-        if autoconnect and not self.bridge.isConnected(profile):
-            #Does the user want autoconnection ?
-            self.asyncConnect(profile, callback=lambda dummy: self.plug_profile_5(watched, autoconnect, profile))
-        else:
-            self.plug_profile_5(watched, autoconnect, profile)
-
-    def plug_profile_5(self, watched, autoconnect, profile):
-        self.profiles[profile]['watched'] = watched.split()  # TODO: put this in a plugin
-
-        ## misc ##
-        self.profiles[profile]['onlineContact'] = set()  # FIXME: temporary
-
-        #TODO: manage multi-profiles here
-        if not self.bridge.isConnected(profile):
-            self.setStatusOnline(False)
-        else:
-            self.setStatusOnline(True)
-
-            ### now we fill the contact list ###
-            for contact in self.bridge.getContacts(profile):
-                self.newContactHandler(*contact, profile=profile)
+    def plug_profiles(self, profiles):
+        """Tell application which profiles must be used
 
-            presences = self.bridge.getPresenceStatuses(profile)
-            for contact in presences:
-                for res in presences[contact]:
-                    jabber_id = ('%s/%s' % (JID(contact).bare, res)) if res else contact
-                    show = presences[contact][res][0]
-                    priority = presences[contact][res][1]
-                    statuses = presences[contact][res][2]
-                    self.presenceUpdateHandler(jabber_id, show, priority, statuses, profile)
-                data = self.bridge.getEntityData(contact, ['avatar', 'nick'], profile)
-                for key in ('avatar', 'nick'):
-                    if key in data:
-                        self.entityDataUpdatedHandler(contact, key, data[key], profile)
+        @param profiles: list of valid profile names
+        """
+        self.plugging_profiles()
+        for profile in profiles:
+            self.profiles.plug(profile)
 
-            #The waiting subscription requests
-            waitingSub = self.bridge.getWaitingSub(profile)
-            for sub in waitingSub:
-                self.subscribeHandler(waitingSub[sub], sub, profile)
+    def plugging_profiles(self):
+        """Method to subclass to manage frontend specific things to do
 
-            #Now we open the MUC window where we already are:
-            for room_args in self.bridge.getRoomsJoined(profile):
-                self.roomJoinedHandler(*room_args, profile=profile)
-
-            for subject_args in self.bridge.getRoomsSubjects(profile):
-                self.roomNewSubjectHandler(*subject_args, profile=profile)
-
-            #Finaly, we get the waiting confirmation requests
-            for confirm_id, confirm_type, data in self.bridge.getWaitingConf(profile):
-                self.askConfirmationHandler(confirm_id, confirm_type, data, profile)
+        will be called when profiles are choosen and are to be plugged soon
+        """
+        pass
 
     def unplug_profile(self, profile):
         """Tell the application to not follow anymore the profile"""
         if not profile in self.profiles:
-            log.warning(_("This profile is not plugged"))
-            return
-        self.profiles.remove(profile)
+            raise ValueError("The profile [{}] is not plugged".format(profile))
+        self.profiles.unplug(profile)
 
     def clear_profile(self):
         self.profiles.clear()
 
+    def addContactList(self, profile):
+        """Method to subclass to add a contact list widget
+
+        will be called on each profile session build
+        @return: a ContactList widget
+        """
+        return NotImplementedError
+
+    def newWidget(self, widget):
+        raise NotImplementedError
+
     def connectedHandler(self, profile):
         """called when the connection is made"""
         log.debug(_("Connected"))
-        self.setStatusOnline(True)
+        self.setStatusOnline(True, profile=profile)
 
     def disconnectedHandler(self, profile):
         """called when the connection is closed"""
         log.debug(_("Disconnected"))
-        self.contact_list.clearContacts()
-        self.setStatusOnline(False)
+        self.contact_lists[profile].clearContacts()
+        self.setStatusOnline(False, profile=profile)
 
     def newContactHandler(self, JabberId, attributes, groups, profile):
-        entity = JID(JabberId)
+        entity = jid.JID(JabberId)
         _groups = list(groups)
-        self.contact_list.replace(entity, _groups, attributes)
+        self.contact_lists[profile].setContact(entity, _groups, attributes, in_roster=True)
 
-    def _newMessage(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
-        """newMessage premanagement: a dirty hack to manage private messages
+    def newMessageHandler(self, from_jid_s, msg, type_, to_jid_s, extra, profile):
+        from_jid = jid.JID(from_jid_s)
+        to_jid = jid.JID(to_jid_s)
 
-        if a private MUC message is detected, from_jid or to_jid is prefixed and resource is escaped
-        """
-        # FIXME: must be refactored for 0.6
-        from_jid = JID(from_jid_s)
-        to_jid = JID(to_jid_s)
+        if not self.trigger.point("newMessageTrigger", from_jid, msg, type_, to_jid, extra, profile=profile):
+            return
 
-        from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
-        win = to_jid if from_me else from_jid
+        from_me = from_jid.bare == self.profiles[profile].whoami.bare
+        target = to_jid if from_me else from_jid
+
+        chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
+        contact_list = self.contact_lists[profile]
+
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, on_new_widget=None, profile=profile)
 
-        if ((type_ != "groupchat" and self.contact_list.getSpecial(win) == "MUC") and
-            (type_ != C.MESS_TYPE_INFO or (type_ == C.MESS_TYPE_INFO and win.resource))):
-            #we have a private message in a MUC room
-            #XXX: normaly we use bare jid as key, here we need the full jid
-            #     so we cheat by replacing the "/" before the resource by
-            #     a "@", so the jid is invalid,
-            new_jid = escapePrivate(win)
-            if from_me:
-                to_jid = new_jid
-            else:
-                from_jid = new_jid
-            if new_jid not in self.contact_list:
-                self.contact_list.add(new_jid, [C.GROUP_NOT_IN_ROSTER])
+        self.current_action_ids = set() # FIXME: to be removed
+        self.current_action_ids_cb = {} # FIXME: to be removed
 
-        self.newMessageHandler(from_jid, to_jid, msg, type_, extra, profile)
+        if not from_jid in contact_list and from_jid.bare != self.profiles[profile].whoami.bare:
+            #XXX: needed to show entities which haven't sent any
+            #     presence information and which are not in roster
+            contact_list.setContact(from_jid)
+
+        # we display the message in the widget
+        chat_widget.newMessage(from_jid, target, msg, type_, extra, profile)
 
-    def newMessageHandler(self, from_jid, to_jid, msg, type_, extra, profile):
-        from_me = from_jid.bare == self.profiles[profile]['whoami'].bare
-        win = to_jid if from_me else from_jid
-
-        self.current_action_ids = set()
-        self.current_action_ids_cb = {}
+        # ContactList alert
+        visible = False
+        for widget in self.visible_widgets:
+            if isinstance(widget, quick_chat.QuickChat) and widget.manageMessage(from_jid, type_):
+                visible = True
+                break
+        if not visible:
+            contact_list.setAlert(from_jid.bare if type_ == C.MESS_TYPE_GROUPCHAT else from_jid)
 
-        timestamp = extra.get('archive')
-        if type_ == C.MESS_TYPE_INFO:
-            self.chat_wins[win.bare].printInfo(msg, timestamp=float(timestamp) if timestamp else None)
-        else:
-            self.chat_wins[win.bare].printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None)
-
-    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key="@NONE@"):
-        if to_jid.startswith(C.PRIVATE_PREFIX):
-            to_jid = unescapePrivate(to_jid)
-            mess_type = "chat"
+    def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, callback=None, errback=None, profile_key=C.PROF_KEY_NONE):
         if callback is None:
-            callback = lambda: None
+            callback = lambda dummy=None: None # 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")
-        self.bridge.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
+
+        if not self.trigger.point("sendMessageTrigger", to_jid, message, subject, mess_type, extra, callback, errback, profile_key=profile_key):
+            return
+
+        self.bridge.sendMessage(unicode(to_jid), message, subject, mess_type, extra, profile_key, callback=callback, errback=errback)
 
     def newAlertHandler(self, msg, title, alert_type, profile):
         assert alert_type in ['INFO', 'ERROR']
         self.showDialog(unicode(msg), unicode(title), alert_type.lower())
 
-    def setStatusOnline(self, online=True, show="", statuses={}):
+    def setStatusOnline(self, online=True, show="", statuses={}, profile=C.PROF_KEY_NONE):
         raise NotImplementedError
 
-    def presenceUpdateHandler(self, jabber_id, show, priority, statuses, profile):
+    def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
 
-        log.debug(_("presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
-              % {'jid': jabber_id, 'show': show, 'priority': priority, 'statuses': statuses, 'profile': profile})
-        from_jid = JID(jabber_id)
+        log.debug(_("presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]")
+              % {'entity': entity_s, C.PRESENCE_SHOW: show, C.PRESENCE_PRIORITY: priority, C.PRESENCE_STATUSES: statuses, 'profile': profile})
+        entity = jid.JID(entity_s)
 
-        if from_jid == self.profiles[profile]['whoami']:
+        if entity == self.profiles[profile].whoami:
             if show == "unavailable":
-                self.setStatusOnline(False)
+                self.setStatusOnline(False, profile=profile)
             else:
-                self.setStatusOnline(True, show, statuses)
+                self.setStatusOnline(True, show, statuses, profile=profile)
             return
 
-        presences = self.profiles[profile].setdefault('presences', {})
-
-        if show != 'unavailable':
-
-            #FIXME: must be moved in a plugin
-            if from_jid.bare in self.profiles[profile].get('watched',[]) and not from_jid.bare in self.profiles[profile]['onlineContact']:
-                self.showAlert(_("Watched jid [%s] is connected !") % from_jid.bare)
+        # #FIXME: must be moved in a plugin
+        # if entity.bare in self.profiles[profile].data.get('watched',[]) and not entity.bare in self.profiles[profile]['onlineContact']:
+        #     self.showAlert(_("Watched jid [%s] is connected !") % entity.bare)
 
-            presences[jabber_id] = {'show': show, 'priority': priority, 'statuses': statuses}
-            self.profiles[profile].setdefault('onlineContact',set()).add(from_jid)  # FIXME onlineContact is useless with CM, must be removed
-
-            #TODO: vcard data (avatar)
-
-        if show == "unavailable" and from_jid in self.profiles[profile].get('onlineContact',set()):
-            try:
-                del presences[jabber_id]
-            except KeyError:
-                pass
-            self.profiles[profile]['onlineContact'].remove(from_jid)
+        self.callListeners('presence', entity, show, priority, statuses, profile=profile)
 
-        # check if the contact is connected with another resource, use the one with highest priority
-        jids = [jid for jid in presences if JID(jid).bare == from_jid.bare]
-        if jids:
-            max_jid = max(jids, key=lambda jid: presences[jid]['priority'])
-            data = presences[max_jid]
-            max_priority = data['priority']
-            if show == "unavailable":  # do not check the priority here, because 'unavailable' has a dummy one
-                from_jid = JID(max_jid)
-                show, priority, statuses = data['show'], data['priority'], data['statuses']
-        if not jids or priority >= max_priority:
-            # case 1: not jids means all resources are disconnected, send the 'unavailable' presence
-            # case 2: update (or confirm) with the values of the resource which takes precedence
-            self.contact_list.updatePresence(from_jid, show, priority, statuses)
-
-    def roomJoinedHandler(self, room_jid, room_nicks, user_nick, profile):
+    def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
         """Called when a MUC room is joined"""
-        log.debug(_("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s") % {'room_jid': room_jid, 'profile': profile, 'users': room_nicks})
-        self.chat_wins[room_jid].setUserNick(user_nick)
-        self.chat_wins[room_jid].setType("group")
-        self.chat_wins[room_jid].id = room_jid
-        self.chat_wins[room_jid].setPresents(list(set([user_nick] + room_nicks)))
-        self.contact_list.setSpecial(JID(room_jid), "MUC", show=True)
+        log.debug("Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.setUserNick(user_nick)
+        chat_widget.id = room_jid  # FIXME: to be removed
+        room_nicks = [unicode(nick) for nick in room_nicks]  # FIXME: should be done in DBus bridge / is that still needed?!
+        nicks = list(set([user_nick] + room_nicks))
+        nicks.sort()
+        chat_widget.setPresents(nicks)
+        self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
 
     def roomLeftHandler(self, room_jid_s, profile):
         """Called when a MUC room is left"""
-        log.debug(_("Room [%(room_jid)s] left by %(profile)s") % {'room_jid': room_jid_s, 'profile': profile})
-        del self.chat_wins[room_jid_s]
-        self.contact_list.remove(JID(room_jid_s))
-
-    def roomUserJoinedHandler(self, room_jid, user_nick, user_data, profile):
-        """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].replaceUser(user_nick)
-            log.debug(_("user [%(user_nick)s] joined room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
-
-    def roomUserLeftHandler(self, room_jid, user_nick, user_data, profile):
-        """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].removeUser(user_nick)
-            log.debug(_("user [%(user_nick)s] left room [%(room_jid)s]") % {'user_nick': user_nick, 'room_jid': room_jid})
-
-    def roomUserChangedNickHandler(self, room_jid, old_nick, new_nick, profile):
-        """Called when an user joined a MUC room"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].changeUserNick(old_nick, new_nick)
-            log.debug(_("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]") % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
+        log.debug("Room [%(room_jid)s] left by %(profile)s" % {'room_jid': room_jid_s, 'profile': profile})
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getWidget(quick_chat.QuickChat, room_jid, profile)
+        if chat_widget:
+            self.widgets.deleteWidget(chat_widget)
+        self.contact_lists[profile].remove(room_jid)
 
-    def roomNewSubjectHandler(self, room_jid, subject, profile):
-        """Called when subject of MUC room change"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].setSubject(subject)
-            log.debug(_("new subject for room [%(room_jid)s]: %(subject)s") % {'room_jid': room_jid, "subject": subject})
-
-    def tarotGameStartedHandler(self, room_jid, referee, players, profile):
-        log.debug(_("Tarot Game Started \o/"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].startGame("Tarot", referee, players)
-            log.debug(_("new Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
-
-    def tarotGameNewHandler(self, room_jid, hand, profile):
-        log.debug(_("New Tarot Game"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").newGame(hand)
-
-    def tarotGameChooseContratHandler(self, room_jid, xml_data, profile):
-        """Called when the player has to select his contrat"""
-        log.debug(_("Tarot: need to select a contrat"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").chooseContrat(xml_data)
-
-    def tarotGameShowCardsHandler(self, room_jid, game_stage, cards, data, profile):
-        log.debug(_("Show cards"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").showCards(game_stage, cards, data)
-
-    def tarotGameYourTurnHandler(self, room_jid, profile):
-        log.debug(_("My turn to play"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").myTurn()
+    def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, profile):
+        """Called when an user joined a MUC room"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.replaceUser(user_nick)
+        log.debug("user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
 
-    def tarotGameScoreHandler(self, room_jid, xml_data, winners, loosers, profile):
-        """Called when the game is finished and the score are updated"""
-        log.debug(_("Tarot: score received"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").showScores(xml_data, winners, loosers)
-
-    def tarotGameCardsPlayedHandler(self, room_jid, player, cards, profile):
-        log.debug(_("Card(s) played (%(player)s): %(cards)s") % {"player": player, "cards": cards})
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").cardsPlayed(player, cards)
-
-    def tarotGameInvalidCardsHandler(self, room_jid, phase, played_cards, invalid_cards, profile):
-        log.debug(_("Cards played are not valid: %s") % invalid_cards)
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Tarot").invalidCards(phase, played_cards, invalid_cards)
-
-    def quizGameStartedHandler(self, room_jid, referee, players, profile):
-        log.debug(_("Quiz Game Started \o/"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].startGame("Quiz", referee, players)
-            log.debug(_("new Quiz game started by [%(referee)s] in room [%(room_jid)s] with %(players)s") % {'referee': referee, 'room_jid': room_jid, 'players': [str(player) for player in players]})
-
-    def quizGameNewHandler(self, room_jid, data, profile):
-        log.debug(_("New Quiz Game"))
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameNewHandler(data)
+    def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, profile):
+        """Called when an user joined a MUC room"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.removeUser(user_nick)
+        log.debug("user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
 
-    def quizGameQuestionHandler(self, room_jid, question_id, question, timer, profile):
-        """Called when a new question is asked"""
-        log.debug(_(u"Quiz: new question: %s") % question)
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameQuestionHandler(question_id, question, timer)
-
-    def quizGamePlayerBuzzedHandler(self, room_jid, player, pause, profile):
-        """Called when a player pushed the buzzer"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGamePlayerBuzzedHandler(player, pause)
+    def roomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
+        """Called when an user joined a MUC room"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.changeUserNick(old_nick, new_nick)
+        log.debug("user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" % {'old_nick': old_nick, 'new_nick': new_nick, 'room_jid': room_jid})
 
-    def quizGamePlayerSaysHandler(self, room_jid, player, text, delay, profile):
-        """Called when a player say something"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGamePlayerSaysHandler(player, text, delay)
-
-    def quizGameAnswerResultHandler(self, room_jid, player, good_answer, score, profile):
-        """Called when a player say something"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameAnswerResultHandler(player, good_answer, score)
-
-    def quizGameTimerExpiredHandler(self, room_jid, profile):
-        """Called when nobody answered the question in time"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameTimerExpiredHandler()
-
-    def quizGameTimerRestartedHandler(self, room_jid, time_left, profile):
-        """Called when the question is not answered, and we still have time"""
-        if room_jid in self.chat_wins:
-            self.chat_wins[room_jid].getGame("Quiz").quizGameTimerRestartedHandler(time_left)
+    def roomNewSubjectHandler(self, room_jid_s, subject, profile):
+        """Called when subject of MUC room change"""
+        room_jid = jid.JID(room_jid_s)
+        chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
+        chat_widget.setSubject(subject)
+        log.debug("new subject for room [%(room_jid)s]: %(subject)s" % {'room_jid': room_jid, "subject": subject})
 
     def chatStateReceivedHandler(self, from_jid_s, state, profile):
-        """Callback when a new chat state is received.
+        """Called when a new chat state is received.
+
         @param from_jid_s: JID of the contact who sent his state, or '@ALL@'
         @param state: new state (string)
         @profile: current profile
         """
+        from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
+        for widget in self.widgets.getWidgets(quick_chat.QuickChat):
+            if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare:
+                widget.updateChatState(from_jid, state)
 
-        if from_jid_s == '@ALL@':
-            target = '@ALL@'
-            nick = C.ALL_OCCUPANTS
-        else:
-            from_jid = JID(from_jid_s)
-            target = from_jid.bare
-            nick = from_jid.resource
+    def personalEventHandler(self, sender, event_type, data):
+        """Called when a PEP event is received.
 
-        for bare in self.chat_wins.keys():
-            if target == '@ALL' or target == bare:
-                chat_win = self.chat_wins[bare]
-                if chat_win.type == 'one2one':
-                    chat_win.updateChatState(state)
-                elif chat_win.type == 'group':
-                    chat_win.updateChatState(state, nick=nick)
+        @param sender (jid.JID): event sender
+        @param event_type (unicode): event type, e.g. 'MICROBLOG' or 'MICROBLOG_DELETE'
+        @param data (dict): event data
+        """
+        # FIXME move some code from Libervia to here and put the magic strings to constants
+        pass
 
     def _subscribe_cb(self, answer, data):
         entity, profile = data
-        if answer:
-            self.bridge.subscription("subscribed", entity.bare, profile_key=profile)
-        else:
-            self.bridge.subscription("unsubscribed", entity.bare, profile_key=profile)
+        type_ = "subscribed" if answer else "unsubscribed"
+        self.bridge.subscription(type_, unicode(entity.bare), profile_key=profile)
 
     def subscribeHandler(self, type, raw_jid, profile):
         """Called when a subsciption management signal is received"""
-        entity = JID(raw_jid)
+        entity = jid.JID(raw_jid)
         if type == "subscribed":
             # this is a subscription confirmation, we just have to inform user
             self.showDialog(_("The contact %s has accepted your subscription") % entity.bare, _('Subscription confirmation'))
@@ -543,7 +617,7 @@
             # this is a subscriptionn request, we have to ask for user confirmation
             self.showDialog(_("The contact %s wants to subscribe to your presence.\nDo you accept ?") % entity.bare, _('Subscription confirmation'), 'yes/no', answer_cb=self._subscribe_cb, answer_data=(entity, profile))
 
-    def showDialog(self, message, title, type="info", answer_cb=None):
+    def showDialog(self, message, title, type="info", answer_cb=None, answer_data=None):
         raise NotImplementedError
 
     def showAlert(self, message):
@@ -553,33 +627,30 @@
         log.debug(_("param update: [%(namespace)s] %(name)s = %(value)s") % {'namespace': namespace, 'name': name, 'value': value})
         if (namespace, name) == ("Connection", "JabberID"):
             log.debug(_("Changing JID to %s") % value)
-            self.profiles[profile]['whoami'] = JID(value)
+            self.profiles[profile].whoami = jid.JID(value)
         elif (namespace, name) == ("Misc", "Watched"):
             self.profiles[profile]['watched'] = value.split()
         elif (namespace, name) == ('General', C.SHOW_OFFLINE_CONTACTS):
-            self.contact_list.showOfflineContacts(C.bool(value))
+            self.contact_lists[profile].showOfflineContacts(C.bool(value))
         elif (namespace, name) == ('General', C.SHOW_EMPTY_GROUPS):
-            self.contact_list.showEmptyGroups(C.bool(value))
+            self.contact_lists[profile].showEmptyGroups(C.bool(value))
 
-    def contactDeletedHandler(self, jid, profile):
-        target = JID(jid)
-        self.contact_list.remove(target)
-        try:
-            self.profiles[profile]['onlineContact'].remove(target.bare)
-        except KeyError:
-            pass
+    def contactDeletedHandler(self, jid_s, profile):
+        target = jid.JID(jid_s)
+        self.contact_lists[profile].remove(target)
 
-    def entityDataUpdatedHandler(self, jid_str, key, value, profile):
-        jid = JID(jid_str)
+    def entityDataUpdatedHandler(self, entity_s, key, value, profile):
+        entity = jid.JID(entity_s)
         if key == "nick":
-            if jid in self.contact_list:
-                self.contact_list.setCache(jid, 'nick', value)
-                self.contact_list.replace(jid)
+            if entity in self.contact_lists[profile]:
+                self.contact_lists[profile].setCache(entity, 'nick', value)
+                self.callListeners('nick', entity, value, profile=profile)
         elif key == "avatar":
-            if jid in self.contact_list:
-                filename = self.bridge.getAvatarFile(value)
-                self.contact_list.setCache(jid, 'avatar', filename)
-                self.contact_list.replace(jid)
+            if entity in self.contact_lists[profile]:
+                def gotFilename(filename):
+                    self.contact_lists[profile].setCache(entity, 'avatar', filename)
+                    self.callListeners('avatar', entity, filename, profile=profile)
+                self.bridge.getAvatarFile(value, callback=gotFilename)
 
     def askConfirmationHandler(self, confirm_id, confirm_type, data, profile):
         raise NotImplementedError
@@ -587,22 +658,32 @@
     def actionResultHandler(self, type, id, data, profile):
         raise NotImplementedError
 
-    def launchAction(self, callback_id, data=None, profile_key="@NONE@"):
-        """ Launch a dynamic action
+    def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
+        """Launch a dynamic action
         @param callback_id: id of the action to launch
         @param data: data needed only for certain actions
-        @param profile_key: %(doc_profile_key)s
+        @param callback: if not None and 'validated' key is present, it will be called with the following parameters:
+            - callback_id
+            - data
+            - profile_key
+        @param profile_key: %(doc_profile)s
 
         """
         raise NotImplementedError
 
+    def disconnect(self, profile):
+        log.info("disconnecting")
+        self.callListeners('disconnect', profile=profile)
+        self.bridge.disconnect(profile)
+
     def onExit(self):
         """Must be called when the frontend is terminating"""
-        #TODO: mange multi-profile here
-        try:
-            if self.bridge.isConnected(self.profile):
-                if self.bridge.getParamA("autodisconnect", "Connection", profile_key=self.profile) == "true":
+        to_unplug = []
+        for profile in self.profiles:
+            if self.bridge.isConnected(profile):
+                if C.bool(self.bridge.getParamA("autodisconnect", "Connection", profile_key=profile)):
                     #The user wants autodisconnection
-                    self.bridge.disconnect(self.profile)
-        except:
-            pass
+                    self.disconnect(profile)
+            to_unplug.append(profile)
+        for profile in to_unplug:
+            self.unplug_profile(profile)