Mercurial > libervia-backend
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)