# HG changeset patch # User Goffi # Date 1422057629 -3600 # Node ID faa1129559b84f6c7e7ca69765eab68caa24eb35 # Parent 653f2e2eea319c71d2bd68bffb223d984be121cd core, frontends: refactoring to base Libervia on QuickFrontend (big mixed commit): /!\ not finished, everything is still instable ! - bridge: DBus bridge has been modified to allow blocking call to be called in the same way as asynchronous calls - bridge: calls with a callback and no errback are now possible, default errback log the error - constants: removed hack to manage presence without OrderedDict, as an OrderedDict like class has been implemented in Libervia - core: getLastResource has been removed and replaced by getMainResource (there is a global better management of resources) - various style improvments: use of constants when possible, fixed variable overlaps, import of module instead of direct class import - frontends: printInfo and printMessage methods in (Quick)Chat are more generic (use of extra instead of timestamp) - frontends: bridge creation and option parsing (command line arguments) are now specified by the frontend in QuickApp __init__ - frontends: ProfileManager manage a more complete plug sequence (some stuff formerly manage in contact_list have moved to ProfileManager) - quick_frontend (quick_widgets): QuickWidgetsManager is now iterable (all widgets are then returned), or can return an iterator on a specific class (return all widgets of this class) with getWidgets - frontends: tools.jid can now be used in Pyjamas, with some care - frontends (XMLUI): profile is now managed - core (memory): big improvment on entities cache management (and specially resource management) - core (params/exceptions): added PermissionError - various fixes and improvments, check diff for more details diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/bridge/DBus.py --- a/frontends/src/bridge/DBus.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/bridge/DBus.py Sat Jan 24 01:00:29 2015 +0100 @@ -102,10 +102,10 @@ async = False if kwargs: - if 'callback' in kwargs and 'errback' in kwargs: + if 'callback' in kwargs: async = True _callback = kwargs.pop('callback') - _errback = kwargs.pop('errback') + _errback = kwargs.pop('errback', lambda failure: log.error(unicode(failure))) elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): async = True args = list(args) @@ -122,141 +122,497 @@ return method(*args, **kwargs) return getPluginMethod - def addContact(self, entity_jid, profile_key="@DEFAULT@"): - return self.db_core_iface.addContact(entity_jid, profile_key) + + def addContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.addContact(entity_jid, profile_key, **kwargs) def asyncConnect(self, profile_key="@DEFAULT@", password='', callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.asyncConnect(profile_key, password, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) def asyncCreateProfile(self, profile, password='', callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.asyncCreateProfile(profile, password, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) def asyncDeleteProfile(self, profile, callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.asyncDeleteProfile(profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) def asyncGetParamA(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return unicode(self.db_core_iface.asyncGetParamA(name, category, attribute, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - def confirmationAnswer(self, id, accepted, data, profile): - return self.db_core_iface.confirmationAnswer(id, accepted, data, profile) + def confirmationAnswer(self, id, accepted, data, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.confirmationAnswer(id, accepted, data, profile, **kwargs) def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.delContact(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) def discoInfos(self, entity_jid, profile_key, callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.discoInfos(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) def discoItems(self, entity_jid, profile_key, callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.discoItems(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def disconnect(self, profile_key="@DEFAULT@"): - return self.db_core_iface.disconnect(profile_key) + def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.disconnect(profile_key, **kwargs) - def getConfig(self, section, name): - return unicode(self.db_core_iface.getConfig(section, name)) + def getConfig(self, section, name, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return unicode(self.db_core_iface.getConfig(section, name, **kwargs)) def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.getContacts(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def getContactsFromGroup(self, group, profile_key="@DEFAULT@"): - return self.db_core_iface.getContactsFromGroup(group, profile_key) + def getContactsFromGroup(self, group, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getContactsFromGroup(group, profile_key, **kwargs) - def getEntityData(self, jid, keys, profile): - return self.db_core_iface.getEntityData(jid, keys, profile) + def getEntityData(self, jid, keys, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getEntityData(jid, keys, profile, **kwargs) def getHistory(self, from_jid, to_jid, limit, between=True, search='', profile="@NONE@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.getHistory(from_jid, to_jid, limit, between, search, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def getLastResource(self, contact_jid, profile_key="@DEFAULT@"): - return unicode(self.db_core_iface.getLastResource(contact_jid, profile_key)) + def getMainResource(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return unicode(self.db_core_iface.getMainResource(contact_jid, profile_key, **kwargs)) - def getMenuHelp(self, menu_id, language): - return unicode(self.db_core_iface.getMenuHelp(menu_id, language)) + def getMenuHelp(self, menu_id, language, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return unicode(self.db_core_iface.getMenuHelp(menu_id, language, **kwargs)) - def getMenus(self, language, security_limit): - return self.db_core_iface.getMenus(language, security_limit) + def getMenus(self, language, security_limit, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getMenus(language, security_limit, **kwargs) - def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@"): - return unicode(self.db_core_iface.getParamA(name, category, attribute, profile_key)) + def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return unicode(self.db_core_iface.getParamA(name, category, attribute, profile_key, **kwargs)) def getParams(self, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return unicode(self.db_core_iface.getParams(security_limit, app, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - def getParamsCategories(self, ): - return self.db_core_iface.getParamsCategories() + def getParamsCategories(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getParamsCategories(**kwargs) def getParamsForCategory(self, category, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return unicode(self.db_core_iface.getParamsForCategory(category, security_limit, app, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) def getParamsUI(self, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return unicode(self.db_core_iface.getParamsUI(security_limit, app, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - def getPresenceStatuses(self, profile_key="@DEFAULT@"): - return self.db_core_iface.getPresenceStatuses(profile_key) + def getPresenceStatuses(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getPresenceStatuses(profile_key, **kwargs) - def getProfileName(self, profile_key="@DEFAULT@"): - return unicode(self.db_core_iface.getProfileName(profile_key)) + def getProfileName(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return unicode(self.db_core_iface.getProfileName(profile_key, **kwargs)) - def getProfilesList(self, ): - return self.db_core_iface.getProfilesList() + def getProfilesList(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getProfilesList(**kwargs) - def getProgress(self, id, profile): - return self.db_core_iface.getProgress(id, profile) + def getProgress(self, id, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getProgress(id, profile, **kwargs) def getReady(self, callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.getReady(timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def getVersion(self, ): - return unicode(self.db_core_iface.getVersion()) + def getVersion(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return unicode(self.db_core_iface.getVersion(**kwargs)) - def getWaitingConf(self, profile_key): - return self.db_core_iface.getWaitingConf(profile_key) + def getWaitingConf(self, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getWaitingConf(profile_key, **kwargs) - def getWaitingSub(self, profile_key="@DEFAULT@"): - return self.db_core_iface.getWaitingSub(profile_key) + def getWaitingSub(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.getWaitingSub(profile_key, **kwargs) - def isConnected(self, profile_key="@DEFAULT@"): - return self.db_core_iface.isConnected(profile_key) + def isConnected(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.isConnected(profile_key, **kwargs) def launchAction(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.launchAction(callback_id, data, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def loadParamsTemplate(self, filename): - return self.db_core_iface.loadParamsTemplate(filename) + def loadParamsTemplate(self, filename, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.loadParamsTemplate(filename, **kwargs) - def paramsRegisterApp(self, xml, security_limit=-1, app=''): - return self.db_core_iface.paramsRegisterApp(xml, security_limit, app) + def paramsRegisterApp(self, xml, security_limit=-1, app='', callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.paramsRegisterApp(xml, security_limit, app, **kwargs) - def saveParamsTemplate(self, filename): - return self.db_core_iface.saveParamsTemplate(filename) + def saveParamsTemplate(self, filename, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.saveParamsTemplate(filename, **kwargs) def sendMessage(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.sendMessage(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): - return self.db_core_iface.setParam(name, value, category, security_limit, profile_key) + def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.setParam(name, value, category, security_limit, profile_key, **kwargs) - def setPresence(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"): - return self.db_core_iface.setPresence(to_jid, show, statuses, profile_key) + def setPresence(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.setPresence(to_jid, show, statuses, profile_key, **kwargs) - def subscription(self, sub_type, entity, profile_key="@DEFAULT@"): - return self.db_core_iface.subscription(sub_type, entity, profile_key) + def subscription(self, sub_type, entity, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.subscription(sub_type, entity, profile_key, **kwargs) def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None): - error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err)) + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/constants.py --- a/frontends/src/constants.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/constants.py Sat Jan 24 01:00:29 2015 +0100 @@ -20,14 +20,7 @@ from sat.core import constants from sat.core.i18n import _, D_ - -try: - from collections import OrderedDict # only available from python 2.7 -except ImportError: - try: - from ordereddict import OrderedDict - except ImportError: - pass # libervia can not import external libraries +from collections import OrderedDict # only available from python 2.7 def getPresence(): @@ -35,25 +28,15 @@ in a method we get a JS runtime SyntaxError: "missing ) in parenthetical". # TODO: merge this definition with those in primitivus.constants and wix.constants """ - try: - presence = OrderedDict([("", _("Online")), - ("chat", _("Free for chat")), - ("away", _("Away from keyboard")), - ("dnd", _("Do not disturb")), - ("xa", _("Extended away"))]) - except TypeError: - presence = {"": _("Online"), - "chat": _("Free for chat"), - "away": _("Away from keyboard"), - "dnd": _("Do not disturb"), - "xa": _("Extended away") - } - return presence class Const(constants.Const): - PRESENCE = getPresence() + PRESENCE = OrderedDict([("", _("Online")), + ("chat", _("Free for chat")), + ("away", _("Away from keyboard")), + ("dnd", _("Do not disturb")), + ("xa", _("Extended away"))]) # from plugin_misc_text_syntaxes SYNTAX_XHTML = "XHTML" @@ -78,3 +61,7 @@ # Roster GROUP_NOT_IN_ROSTER = D_('Not in roster') + + #Chats + CHAT_ONE2ONE = 'one2one' + CHAT_GROUP = 'group' diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/jp/base.py --- a/frontends/src/jp/base.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/jp/base.py Sat Jan 24 01:00:29 2015 +0100 @@ -245,11 +245,11 @@ callback() def get_full_jid(self, param_jid): - """Return the full jid if possible (add last resource when find a bare jid)""" + """Return the full jid if possible (add main resource when find a bare jid)""" _jid = JID(param_jid) if not _jid.resource: #if the resource is not given, we try to add the last known resource - last_resource = self.bridge.getLastResource(param_jid, self.profile) + last_resource = self.bridge.getMainResource(param_jid, self.profile) if last_resource: return "%s/%s" % (_jid.bare, last_resource) return param_jid diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/primitivus/chat.py --- a/frontends/src/primitivus/chat.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/primitivus/chat.py Sat Jan 24 01:00:29 2015 +0100 @@ -30,7 +30,7 @@ from sat_frontends.primitivus.keys import action_key_map as a_key from sat_frontends.primitivus.widget import PrimitivusWidget import time -from sat_frontends.tools.jid import JID +from sat_frontends.tools import jid class ChatText(urwid.FlowWidget): @@ -83,7 +83,7 @@ class Chat(PrimitivusWidget, QuickChat): - def __init__(self, host, target, type_='one2one', profiles=None): + def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None): QuickChat.__init__(self, host, target, type_, profiles=profiles) self.content = urwid.SimpleListWalker([]) self.text_list = urwid.ListBox(self.content) @@ -93,10 +93,10 @@ self.pile = urwid.Pile([self.chat_colums]) PrimitivusWidget.__init__(self, self.pile, self.target) - # we must adapt the behavious with the type - if type_ == 'one2one': + # we must adapt the behaviour with the type + if type_ == C.CHAT_ONE2ONE: self.historyPrint(profile=self.profile) - elif type_ == 'group': + elif type_ == C.CHAT_GROUP: if len(self.chat_colums.contents) == 1: present_widget = self._buildPresentList() self.present_panel = sat_widgets.VerticalSeparator(present_widget) @@ -110,7 +110,7 @@ def keypress(self, size, key): if key == a_key['OCCUPANTS_HIDE']: #user wants to (un)hide the presents panel - if self.type == 'group': + if self.type == C.CHAT_GROUP: widgets = [widget for (widget, options) in self.chat_colums.contents] if self.present_panel in widgets: self._removePresentPanel() @@ -140,11 +140,11 @@ def getMenu(self): """Return Menu bar""" menu = sat_widgets.Menu(self.host.loop) - if self.type == 'group': + if self.type == C.CHAT_GROUP: self.host.addMenus(menu, C.MENU_ROOM, {'room_jid': self.target.bare}) game = _("Game") menu.addMenu(game, "Tarot", self.onTarotRequest) - elif self.type == 'one2one': + elif self.type == C.CHAT_ONE2ONE: self.host.addMenus(menu, C.MENU_SINGLE, {'jid': self.target}) menu.addMenu(_("Action"), _("Send file"), self.onSendFileRequest) return menu @@ -170,13 +170,13 @@ self.title_dynamic = '({})'.format(state) def _presentClicked(self, list_wid, clicked_wid): - assert self.type == 'group' + assert self.type == C.CHAT_GROUP nick = clicked_wid.getValue().value if nick == self.getUserNick(): #We ignore clicks on our own nick return contact_list = self.host.contact_lists[self.profile] - full_jid = JID("%s/%s" % (self.target.bare, nick)) + full_jid = jid.JID("%s/%s" % (self.target.bare, nick)) #we have a click on a nick, we need to create the widget if it doesn't exists self.getOrCreatePrivateWidget(full_jid) @@ -258,10 +258,16 @@ def onPrivateCreated(self, widget): self.host.contact_lists[widget.profile].specialResourceVisible(widget.target) - def printMessage(self, from_jid, msg, profile, timestamp=None): - assert isinstance(from_jid, JID) + def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE): + assert isinstance(from_jid, jid.JID) + if extra is None: + extra = {} try: - jid, nick, mymess = QuickChat.printMessage(self, from_jid, msg, profile, timestamp) + timestamp = float(extra['timestamp']) + except KeyError: + timestamp=None + try: + nick, mymess = QuickChat.printMessage(self, from_jid, msg, extra, profile) except TypeError: # None is returned, the message is managed return @@ -291,7 +297,7 @@ # all messages and not only with the messages coming from the history. self._notify(from_jid, msg) - def printInfo(self, msg, type_='normal', timestamp=None): + def printInfo(self, msg, type_='normal', extra=None): """Print general info @param msg: message to print @type_: one of: @@ -299,6 +305,12 @@ me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" @param timestamp (float): number of seconds since epoch """ + if extra is None: + extra = {} + try: + timestamp = float(extra['timestamp']) + except KeyError: + timestamp=None _widget = ChatText(self, timestamp, None, False, msg, is_info=True) self.content.append(_widget) self._notify(msg=msg) @@ -316,7 +328,7 @@ self.text_list.focus_position = len(self.content) - 1 self.host.redraw() if not self.host.x_notify.hasFocus(): - if self.type == "one2one": + if self.type == C.CHAT_ONE2ONE: self.host.x_notify.sendNotification(_("Primitivus: %s is talking to you") % from_jid) elif self.getUserNick().lower() in msg.lower(): self.host.x_notify.sendNotification(_("Primitivus: %(user)s mentioned you in room '%(room)s'") % {'user': from_jid, 'room': self.target}) @@ -356,9 +368,9 @@ self.host.showDialog(_(u"File has a unicode error in its name, it's not yet managed by SàT"), title=_("Can't send file"), type_="error") return #FIXME: check last_resource: what if self.target.resource exists ? - last_resource = self.host.bridge.getLastResource(unicode(self.target.bare), self.profile) + last_resource = self.host.bridge.getMainResource(unicode(self.target.bare), self.profile) if last_resource: - full_jid = JID("%s/%s" % (self.target.bare, last_resource)) + full_jid = jid.JID("%s/%s" % (self.target.bare, last_resource)) else: full_jid = self.target progress_id = self.host.bridge.sendFile(full_jid, filepath, {}, self.profile) diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/primitivus/contact_list.py --- a/frontends/src/primitivus/contact_list.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/primitivus/contact_list.py Sat Jan 24 01:00:29 2015 +0100 @@ -148,6 +148,10 @@ self.update() self._emit('click', entity) + def updatePresence(self, entity, show, priority, statuses): + super(ContactList, self).updatePresence(entity, show, priority, statuses) + self.update() + # Methods to build the widget def _buildEntityWidget(self, entity, keys=None, use_bare_jid=False, with_alert=True, with_show_attr=True, markup_prepend=None, markup_append = None): diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/primitivus/primitivus --- a/frontends/src/primitivus/primitivus Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/primitivus/primitivus Sat Jan 24 01:00:29 2015 +0100 @@ -27,8 +27,9 @@ import urwid from urwid_satext import sat_widgets from urwid_satext.files_management import FileDialog +from sat_frontends.bridge.DBus import DBusBridgeFrontend from sat_frontends.quick_frontend.quick_app import QuickApp -from sat_frontends.quick_frontend.quick_utils import getNewPath +from sat_frontends.quick_frontend import quick_utils from sat_frontends.quick_frontend import quick_chat from sat_frontends.primitivus.profile_manager import ProfileManager from sat_frontends.primitivus.contact_list import ContactList @@ -277,14 +278,14 @@ class PrimitivusApp(QuickApp, InputHistory): def __init__(self): - QuickApp.__init__(self) + QuickApp.__init__(self, create_bridge=DBusBridgeFrontend, check_options=quick_utils.check_options) ## main loop setup ## self.main_widget = ProfileManager(self) self.loop = urwid.MainLoop(self.main_widget, C.PALETTE, event_loop=urwid.GLibEventLoop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler) - ##misc setup## + self._visible_widgets = set() self.notif_bar = sat_widgets.NotificationBar() urwid.connect_signal(self.notif_bar, 'change', self.onNotification) @@ -298,6 +299,10 @@ signal.signal(signal.SIGINT, signal.SIG_IGN) @property + def visible_widgets(self): + return self._visible_widgets + + @property def mode(self): return self.editBar.mode @@ -496,14 +501,14 @@ self.redraw() def newWidget(self, widget): + self.selectWidget(widget) + + def selectWidget(self, widget): """Display a widget if possible, else add it in the notification bar queue @param widget: BoxWidget """ - self.selectWidget(widget) - - def selectWidget(self, widget): assert len(self.center_part.widget_list)<=2 wid_idx = len(self.center_part.widget_list)-1 self.center_part.widget_list[wid_idx] = widget @@ -512,7 +517,7 @@ except KeyError: log.debug("No menu to delete") self.selected_widget = widget - self.visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now + self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now for contact_list in self.contact_lists.itervalues(): contact_list.unselectAll() @@ -642,7 +647,7 @@ def dir_selected_cb(path): dest_path = join(path, data['filename']) - answer_data["dest_path"] = getNewPath(dest_path) + answer_data["dest_path"] = quick_utils.getNewPath(dest_path) self.addProgress(confirmation_id, dest_path) accept_cb(None) diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/quick_frontend/constants.py --- a/frontends/src/quick_frontend/constants.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/quick_frontend/constants.py Sat Jan 24 01:00:29 2015 +0100 @@ -22,10 +22,6 @@ class Const(constants.Const): - #Chats - CHAT_ONE2ONE = 'one2one' - CHAT_GROUP = 'group' - #Contact list CONTACT_GROUPS = 'groups' CONTACT_RESOURCES = 'resources' diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/quick_frontend/quick_app.py --- a/frontends/src/quick_frontend/quick_app.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/quick_frontend/quick_app.py Sat Jan 24 01:00:29 2015 +0100 @@ -22,11 +22,9 @@ from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions -from sat_frontends.bridge.DBus import DBusBridgeFrontend from sat_frontends.tools import jid from sat_frontends.quick_frontend.quick_widgets import QuickWidgetsManager from sat_frontends.quick_frontend import quick_chat -from optparse import OptionParser from sat_frontends.quick_frontend.constants import Const as C @@ -78,20 +76,54 @@ contact_list.fill() #The waiting subscription requests - waitingSub = self.bridge.getWaitingSub(self.profile) - for sub in waitingSub: - self.host.subscribeHandler(waitingSub[sub], sub, self.profile) + self.bridge.getWaitingSub(self.profile, callback=self._plug_profile_gotWaitingSub) + + 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) + - #Now we open the MUC window where we already are: - for room_args in self.bridge.getRoomsJoined(self.profile): - self.host.roomJoinedHandler(*room_args, profile=self.profile) + 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 subject_args in self.bridge.getRoomsSubjects(self.profile): - self.host.roomNewSubjectHandler(*subject_args, profile=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 - for confirm_id, confirm_type, data in self.bridge.getWaitingConf(self.profile): - self.host.askConfirmationHandler(confirm_id, confirm_type, data, self.profile) + #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")) @@ -132,20 +164,27 @@ class QuickApp(object): """This class contain the main methods needed for the frontend""" - def __init__(self): + 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) + """ ProfileManager.host = self self.profiles = ProfilesManager() self.contact_lists = {} self.widgets = QuickWidgetsManager(self) - self.check_options() + if check_options is not None: + self.options = check_options() + else: + self.options = None # widgets - self.visible_widgets = set() # widgets currently visible (must be filled by frontend) self.selected_widget = None # widget currently selected (must be filled by frontend) ## 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) @@ -202,6 +241,11 @@ 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, functionName, handler=None, iface="core", with_profile=True): """Register a handler for a signal @@ -211,7 +255,7 @@ @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(functionName, 'Handler')) if not with_profile: self.bridge.register(functionName, handler, iface) return @@ -236,24 +280,15 @@ @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager """ - if self.options.profile: + if self.options and self.options.profile: profile_manager.autoconnect([self.options.profile]) - def check_options(self): - """Check command line options""" - usage = _(""" - %prog [options] + def profilePlugged(self, profile): + """Method called when the profile is fully plugged, to launch frontend specific workflow - %prog --help for options list - """) - parser = OptionParser(usage=usage) # TODO: use argparse - - 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 + @param profile(unicode): %(doc_profile)s + """ + pass def asyncConnect(self, profile, callback=None, errback=None): if not callback: @@ -281,7 +316,7 @@ will be called when profiles are choosen and are to be plugged soon """ - raise NotImplementedError + pass def unplug_profile(self, profile): """Tell the application to not follow anymore the profile""" @@ -292,6 +327,14 @@ 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 @@ -331,7 +374,7 @@ 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) @@ -500,10 +543,9 @@ @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.visible_widgets: - if isinstance(widget, quick_chat.QuickChat): - if from_jid == C.ENTITY_ALL or from_jid.bare == widget.target.bare: - widget.updateChatState(from_jid, state) + 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) def _subscribe_cb(self, answer, data): entity, profile = data @@ -554,8 +596,9 @@ self.contact_lists[profile].setCache(entity, 'nick', value) elif key == "avatar": if entity in self.contact_lists[profile]: - filename = self.bridge.getAvatarFile(value) - self.contact_lists[profile].setCache(entity, 'avatar', filename) + def gotFilename(filename): + self.contact_lists[profile].setCache(entity, 'avatar', filename) + self.bridge.getAvatarFile(value, callback=gotFilename) def askConfirmationHandler(self, confirm_id, confirm_type, data, profile): raise NotImplementedError diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/quick_frontend/quick_chat.py --- a/frontends/src/quick_frontend/quick_chat.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/quick_frontend/quick_chat.py Sat Jan 24 01:00:29 2015 +0100 @@ -20,11 +20,18 @@ from sat.core.i18n import _ from sat.core.log import getLogger log = getLogger(__name__) -from sat_frontends.tools.jid import JID +from sat_frontends.tools import jid from sat_frontends.quick_frontend import quick_widgets from sat_frontends.quick_frontend.constants import Const as C +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 = lambda x: str(x) + + class QuickChat(quick_widgets.QuickWidget): def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None): @@ -153,11 +160,11 @@ def onHistory(history): for line in history: - timestamp, from_jid, to_jid, message, _type, extra = line - if ((self.type == C.CHAT_GROUP and _type != C.MESS_TYPE_GROUPCHAT) or - (self.type == C.CHAT_ONE2ONE and _type == C.MESS_TYPE_GROUPCHAT)): + timestamp, from_jid, to_jid, message, type_, extra = line # FIXME: extra is unused ! + if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or + (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)): continue - self.printMessage(JID(from_jid), message, profile, timestamp) + self.printMessage(jid.JID(from_jid), message, {'timestamp':timestamp}, profile) self.afterHistoryPrint() def onHistoryError(err): @@ -165,7 +172,7 @@ target = self.target.bare - return self.host.bridge.getHistory(self.host.profiles[profile].whoami.bare, target, size, search=search, profile=profile, callback=onHistory, errback=onHistoryError) + self.host.bridge.getHistory(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=onHistory, errback=onHistoryError) def _get_nick(self, entity): """Return nick of this entity when possible""" @@ -193,29 +200,27 @@ chat_widget = self.getOrCreatePrivateWidget(target) chat_widget.newMessage(from_jid, target, msg, type_, extra, profile) else: - timestamp = extra.get('archive') if type_ == C.MESS_TYPE_INFO: - self.printInfo(msg, timestamp=float(timestamp) if timestamp else None) + self.printInfo(msg, extra=extra) else: - self.printMessage(from_jid, msg, profile, float(timestamp) if timestamp else None) + self.printMessage(from_jid, msg, extra, profile) - def printMessage(self, from_jid, msg, profile, timestamp=None): + def printMessage(self, from_jid, msg, extra=None, profile=C.PROF_KEY_NONE): """Print message in chat window. Must be implemented by child class""" - jid = JID(from_jid) - nick = self._get_nick(jid) - mymess = (jid.resource == self.nick) if self.type == C.CHAT_GROUP else (jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user + nick = self._get_nick(from_jid) + mymess = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare) #mymess = True if message comes from local user if msg.startswith('/me '): - self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', timestamp=timestamp) + self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', extra=extra) return - return jid, nick, mymess + return nick, mymess - def printInfo(self, msg, type_='normal', timestamp=None): + def printInfo(self, msg, type_='normal', extra=None): """Print general info @param msg: message to print @type_: one of: normal: general info like "toto has joined the room" me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist" - @param timestamp (float): number of seconds since epoch + @param extra (dict): message data """ raise NotImplementedError diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/quick_frontend/quick_contact_list.py --- a/frontends/src/quick_frontend/quick_contact_list.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/quick_frontend/quick_contact_list.py Sat Jan 24 01:00:29 2015 +0100 @@ -22,7 +22,18 @@ log = getLogger(__name__) from sat_frontends.quick_frontend.quick_widgets import QuickWidget from sat_frontends.quick_frontend.constants import Const as C -from sat_frontends.tools import jid + + +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 + # XXX: pyjamas' max doesn't support key argument, so we implement it ourself + pyjamas_max = max + def max(iterable, key): + iter_cpy = list(iterable) + iter_cpy.sort(key=key) + return pyjamas_max(iter_cpy) class QuickContactList(QuickWidget): @@ -78,18 +89,6 @@ for contact in contacts: self.host.newContactHandler(*contact, profile=self.profile) - presences = self.host.bridge.getPresenceStatuses(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) - data = self.host.bridge.getEntityData(contact, ['avatar', 'nick'], self.profile) - for key in ('avatar', 'nick'): - if key in data: - self.host.entityDataUpdatedHandler(contact, key, data[key], self.profile) self.host.bridge.getContacts(self.profile, callback=gotContacts) def update(self): @@ -105,7 +104,12 @@ - if resource is given, it is used @param name(unicode): name the data to get, or None to get everything """ - cache = self._cache[entity.bare] + try: + cache = self._cache[entity.bare] + except KeyError: + self.setContact(entity) + cache = self._cache[entity.bare] + if name is None: return cache try: @@ -280,10 +284,6 @@ set_.difference_update(to_remove) self.update() - def add(self, jid, param_groups=None): - """add a contact to the list""" - raise NotImplementedError - def updatePresence(self, entity, show, priority, statuses): """Update entity's presence status @@ -311,7 +311,6 @@ priority_resource = max(resources_data, key=lambda res: resources_data[res][C.PRESENCE_PRIORITY]) cache[C.CONTACT_MAIN_RESOURCE] = priority_resource - self.update() def unselectAll(self): """Unselect all contacts""" diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/quick_frontend/quick_utils.py --- a/frontends/src/quick_frontend/quick_utils.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/quick_frontend/quick_utils.py Sat Jan 24 01:00:29 2015 +0100 @@ -17,7 +17,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from sat.core.i18n import _ from os.path import exists, splitext +from optparse import OptionParser def getNewPath(path): """ Check if path exists, and find a non existant path if needed """ @@ -31,3 +33,19 @@ return new_path idx+=1 +def check_options(): + """Check command line options""" + usage = _(""" + %prog [options] + + %prog --help for options list + """) + parser = OptionParser(usage=usage) # TODO: use argparse + + parser.add_option("-p", "--profile", help=_("Select the profile to use")) + + (options, args) = parser.parse_args() + if options.profile: + options.profile = options.profile.decode('utf-8') + return options + diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/quick_frontend/quick_widgets.py --- a/frontends/src/quick_frontend/quick_widgets.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/quick_frontend/quick_widgets.py Sat Jan 24 01:00:29 2015 +0100 @@ -25,6 +25,13 @@ classes_map = {} +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 + + def register(base_cls, child_cls=None): """Register a child class to use by default when a base class is needed @@ -43,6 +50,40 @@ self.host = host self._widgets = {} + def __iter__(self): + """Iterate throught all widgets""" + for widget_map in self._widgets.itervalues(): + for widget in widget_map.itervalues(): + yield widget + + def getRealClass(self, class_): + """Return class registered for given class_ + + @param class_: subclass of QuickWidget + @return: class actually used to create widget + """ + try: + cls = classes_map[class_] + except KeyError: + cls = class_ + if cls is None: + raise exceptions.InternalError("There is not class registered for {}".format(class_)) + return cls + + def getWidgets(self, class_): + """Get all subclassed widgets + + @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] + @return: iterator on widgets + """ + class_ = self.getRealClass(class_) + try: + widgets_map = self._widgets[class_] + except KeyError: + return iter([]) + else: + return widgets_map.itervalues() + def getOrCreateWidget(self, class_, target, *args, **kwargs): """Get an existing widget or create a new one when necessary @@ -60,13 +101,7 @@ if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash @return: a class_ instance, either new or already existing """ - # class management - try: - cls = classes_map[class_] - except KeyError: - cls = class_ - if cls is None: - raise exceptions.InternalError("There is not class registered for {}".format(class_)) + cls = self.getRealClass(class_) # arguments management _args = [self.host, target] + list(args) or [] # FIXME: check if it's really necessary to use optional args @@ -86,12 +121,12 @@ hash_ = cls.getWidgetHash(target, _kwargs['profiles']) # widget creation or retrieval - widgets_list = self._widgets.setdefault(cls, {}) # we sorts widgets by classes + widgets_map = self._widgets.setdefault(cls, {}) # we sorts widgets by classes if not cls.SINGLE: widget = None # if the class is not SINGLE, we always create a new widget else: try: - widget = widgets_list[hash_] + widget = widgets_map[hash_] widget.addTarget(target) except KeyError: widget = None @@ -106,7 +141,7 @@ log.debug(u"Creating new widget for target {} {}".format(target, cls)) widget = cls(*_args, **_kwargs) - widgets_list[hash_] = widget + widgets_map[hash_] = widget if on_new_widget == 'NEW_WIDGET': self.host.newWidget(widget) diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/tools/jid.py --- a/frontends/src/tools/jid.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/tools/jid.py Sat Jan 24 01:00:29 2015 +0100 @@ -18,20 +18,84 @@ # along with this program. If not, see . -class JID(unicode): +# hack to use this module with pyjamas +try: + unicode('') # XXX: unicode doesn't exist in pyjamas + + # normal version + class BaseJID(unicode): + def __new__(cls, jid_str): + self = unicode.__new__(cls, cls._normalize(jid_str)) + return self + + def __init__(self, jid_str): + pass + + def _parse(self): + """Find node domain and resource""" + node_end = self.find('@') + if node_end < 0: + node_end = 0 + domain_end = self.find('/') + if domain_end == 0: + raise ValueError("a jid can't start with '/'") + if domain_end == -1: + domain_end = len(self) + self.node = self[:node_end] or None + self.domain = self[(node_end + 1) if node_end else 0:domain_end] + self.resource = self[domain_end + 1:] or None + +except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options + # pyjamas version + class BaseJID(object): + def __init__(self, jid_str): + self.__internal_str = JID._normalize(jid_str) + + def __str__(self): + return self.__internal_str + + def __getattr__(self, name): + return getattr(self.__internal_str, name) + + def __eq__(self, other): + return (self.node == other.node + and self.domain == other.domain + and self.resource == other.resource) + + def __hash__(self): + return hash(self.__internal_str) + + def find(self, *args): + return self.__internal_str.find(*args) + + def _parse(self): + """Find node domain and resource""" + node_end = self.__internal_str.find('@') + if node_end < 0: + node_end = 0 + domain_end = self.__internal_str.find('/') + if domain_end == 0: + raise ValueError("a jid can't start with '/'") + if domain_end == -1: + domain_end = len(self.__internal_str) + self.node = self.__internal_str[:node_end] or None + self.domain = self.__internal_str[(node_end + 1) if node_end else 0:domain_end] + self.resource = self.__internal_str[domain_end + 1:] or None + + +class JID(BaseJID): """This class help manage JID (Node@Domaine/Resource)""" - def __new__(cls, jid): - self = unicode.__new__(cls, cls._normalize(jid)) + def __init__(self, jid_str): + super(JID, self).__init__(jid_str) self._parse() - return self - @classmethod - def _normalize(cls, jid): + @staticmethod + def _normalize(jid_str): """Naive normalization before instantiating and parsing the JID""" - if not jid: - return jid - tokens = jid.split('/') + if not jid_str: + return jid_str + tokens = jid_str.split('/') tokens[0] = tokens[0].lower() # force node and domain to lower-case return '/'.join(tokens) @@ -41,20 +105,6 @@ return JID(self.domain) return JID(u"{}@{}".format(self.node, self.domain)) - def _parse(self): - """Find node domain and resource""" - node_end = self.find('@') - if node_end < 0: - node_end = 0 - domain_end = self.find('/') - if domain_end == 0: - raise ValueError("a jid can't start with '/'") - if domain_end == -1: - domain_end = len(self) - self.node = self[:node_end] or None - self.domain = self[(node_end + 1) if node_end else 0:domain_end] - self.resource = self[domain_end + 1:] or None - def is_valid(self): """ @return: True if the JID is XMPP compliant diff -r 653f2e2eea31 -r faa1129559b8 frontends/src/tools/xmlui.py --- a/frontends/src/tools/xmlui.py Sat Jan 24 00:15:01 2015 +0100 +++ b/frontends/src/tools/xmlui.py Sat Jan 24 01:00:29 2015 +0100 @@ -210,7 +210,7 @@ @param title: force the title, or use XMLUI one if None @param flags: list of string which can be: - NO_CANCEL: the UI can't be cancelled - @param callback: if present, will be use with launchAction + @param callback: if present, will be used with launchAction """ self.host = host top=parsed_dom.documentElement @@ -289,7 +289,7 @@ self._main_cont = value def _parseChilds(self, _xmlui_parent, current_node, wanted = ('container',), data = None): - """Recursively parse childNodes of an elemen + """Recursively parse childNodes of an element @param _xmlui_parent: widget container with '_xmluiAppend' method @param current_node: element from which childs will be parsed @@ -695,7 +695,7 @@ def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, callback=None, profile=C.PROF_KEY_NONE): """ - @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one + @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one @param dom_free: method used to free the parsed DOM """ if dom_parse is None: diff -r 653f2e2eea31 -r faa1129559b8 src/bridge/DBus.py --- a/src/bridge/DBus.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/bridge/DBus.py Sat Jan 24 01:00:29 2015 +0100 @@ -285,8 +285,8 @@ @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='s', async_callbacks=None) - def getLastResource(self, contact_jid, profile_key="@DEFAULT@"): - return self._callback("getLastResource", unicode(contact_jid), unicode(profile_key)) + def getMainResource(self, contact_jid, profile_key="@DEFAULT@"): + return self._callback("getMainResource", unicode(contact_jid), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='s', diff -r 653f2e2eea31 -r faa1129559b8 src/bridge/bridge_constructor/bridge_constructor.py --- a/src/bridge/bridge_constructor/bridge_constructor.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/bridge/bridge_constructor/bridge_constructor.py Sat Jan 24 01:00:29 2015 +0100 @@ -446,17 +446,35 @@ 'args': self.getArguments(function['sig_in'], name=arg_doc, default=default)} if function["type"] == "method": + # XXX: we can manage blocking call in the same way as async one: if callback is None the call will be blocking completion['debug'] = "" if not self.options.debug else 'log.debug ("%s")\n%s' % (section, 8 * ' ') completion['args_result'] = self.getArguments(function['sig_in'], name=arg_doc) - completion['async_args'] = 'callback=None, errback=None' if async else '' - completion['async_comma'] = ', ' if async and function['sig_in'] else '' - completion['error_handler'] = ("error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))\n" + 8*" ") if async else '' - completion['async_args_result'] = 'timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler' if async else '' + completion['async_args'] = 'callback=None, errback=None' + completion['async_comma'] = ', ' if function['sig_in'] else '' + completion['error_handler'] = """if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + """ + if async: + completion['blocking_call'] = '' + completion['async_args_result'] = 'timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler' + else: + # XXX: To have a blocking call, we must have not reply_handler, so we test if callback exists, and add reply_handler only in this case + completion['blocking_call'] = """kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + """ + completion['async_args_result'] = '**kwargs' result = "self.db_%(category)s_iface.%(name)s(%(args_result)s%(async_comma)s%(async_args_result)s)" % completion completion['result'] = ("unicode(%s)" if self.options.unicode and function['sig_out'] == 's' else "%s") % result methods_part.append("""\ def %(name)s(self, %(args)s%(async_comma)s%(async_args)s): - %(error_handler)s%(debug)sreturn %(result)s + %(error_handler)s%(blocking_call)s%(debug)sreturn %(result)s """ % completion) #at this point, methods_part should be filled, diff -r 653f2e2eea31 -r faa1129559b8 src/bridge/bridge_constructor/bridge_template.ini --- a/src/bridge/bridge_constructor/bridge_template.ini Sat Jan 24 00:15:01 2015 +0100 +++ b/src/bridge/bridge_constructor/bridge_template.ini Sat Jan 24 01:00:29 2015 +0100 @@ -279,7 +279,7 @@ doc_param_1=%(doc_profile_key)s doc_return=array of jids -[getLastResource] +[getMainResource] type=method category=core sig_in=ss @@ -288,7 +288,7 @@ doc=Return the last resource connected for a contact doc_param_0=contact_jid: jid of the contact doc_param_1=%(doc_profile_key)s -doc_return=the last resource connected of the contact, or "" +doc_return=the resource connected of the contact with highest priority, or "" [getPresenceStatuses] type=method diff -r 653f2e2eea31 -r faa1129559b8 src/bridge/bridge_constructor/dbus_frontend_template.py --- a/src/bridge/bridge_constructor/dbus_frontend_template.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/bridge/bridge_constructor/dbus_frontend_template.py Sat Jan 24 01:00:29 2015 +0100 @@ -102,10 +102,10 @@ async = False if kwargs: - if 'callback' in kwargs and 'errback' in kwargs: + if 'callback' in kwargs: async = True _callback = kwargs.pop('callback') - _errback = kwargs.pop('errback') + _errback = kwargs.pop('errback', lambda failure: log.error(unicode(failure))) elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): async = True args = list(args) @@ -122,4 +122,5 @@ return method(*args, **kwargs) return getPluginMethod + ##METHODS_PART## diff -r 653f2e2eea31 -r faa1129559b8 src/core/constants.py --- a/src/core/constants.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/core/constants.py Sat Jan 24 01:00:29 2015 +0100 @@ -76,7 +76,8 @@ PROF_KEY_NONE = '@NONE@' PROF_KEY_DEFAULT = '@DEFAULT@' ENTITY_ALL = '@ALL@' - ENTITY_LAST_RESOURCE = 'LAST_RESOURCE' + ENTITY_ALL_RESOURCES = '@ALL_RESOURCES@' + ENTITY_MAIN_RESOURCE = '@MAIN_RESOURCE@' ENTITY_CAP_HASH = 'CAP_HASH' diff -r 653f2e2eea31 -r faa1129559b8 src/core/exceptions.py --- a/src/core/exceptions.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/core/exceptions.py Sat Jan 24 01:00:29 2015 +0100 @@ -94,5 +94,9 @@ pass +class PermissionError(Exception): + pass + + class SkipHistory(Exception): # used in MessageReceivedTrigger to avoid history writting pass diff -r 653f2e2eea31 -r faa1129559b8 src/core/sat_main.py --- a/src/core/sat_main.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/core/sat_main.py Sat Jan 24 01:00:29 2015 +0100 @@ -90,7 +90,7 @@ self.bridge.register("disconnect", self.disconnect) self.bridge.register("getContacts", self.getContacts) self.bridge.register("getContactsFromGroup", self.getContactsFromGroup) - self.bridge.register("getLastResource", self.memory._getLastResource) + self.bridge.register("getMainResource", self.memory._getMainResource) self.bridge.register("getPresenceStatuses", self.memory._getPresenceStatuses) self.bridge.register("getWaitingSub", self.memory.getWaitingSub) self.bridge.register("getWaitingConf", self.getWaitingConf) @@ -482,7 +482,7 @@ # we may have a groupchat message, we check if the we know this jid try: entity_type = self.memory.getEntityData(mess_data["to"], ['type'], profile)["type"] - #FIXME: should entity_type manage ressources ? + #FIXME: should entity_type manage resources ? except (exceptions.UnknownEntityError, KeyError): entity_type = "contact" @@ -500,7 +500,7 @@ if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile): return defer.succeed(None) - log.debug(_("Sending jabber message of type [%(type)s] to %(to)s...") % {"type": mess_data["type"], "to": to_jid.full()}) + log.debug(_(u"Sending message (type {type}, to {to})").format(type=mess_data["type"], to=to_jid.full())) def cancelErrorTrap(failure): """A message sending can be cancelled by a plugin treatment""" diff -r 653f2e2eea31 -r faa1129559b8 src/memory/disco.py --- a/src/memory/disco.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/memory/disco.py Sat Jan 24 01:00:29 2015 +0100 @@ -102,9 +102,7 @@ jid_ = jid.JID(client.jid.host) try: cap_hash = self.host.memory.getEntityData(jid_, [C.ENTITY_CAP_HASH], client.profile)[C.ENTITY_CAP_HASH] - disco_infos = self.hashes[cap_hash] - return defer.succeed(disco_infos) - except KeyError: + except (KeyError, exceptions.UnknownEntityError): # capability hash is not available, we'll compute one def infosCb(disco_infos): cap_hash = self.generateHash(disco_infos) @@ -114,6 +112,9 @@ d = client.disco.requestInfo(jid_) d.addCallback(infosCb) return d + else: + disco_infos = self.hashes[cap_hash] + return defer.succeed(disco_infos) @defer.inlineCallbacks def getItems(self, jid_=None, nodeIdentifier='', profile_key=C.PROF_KEY_NONE): @@ -131,7 +132,7 @@ try: items = self.host.memory.getEntityData(jid_, ["DISCO_ITEMS"], client.profile)["DISCO_ITEMS"] log.debug("[%s] disco items are in cache" % jid_.full()) - except KeyError: + except (KeyError, exceptions.UnknownEntityError): log.debug("Caching [%s] disco items" % jid_.full()) items = yield client.disco.requestItems(jid_, nodeIdentifier) self.host.memory.updateEntityData(jid_, "DISCO_ITEMS", items, client.profile) diff -r 653f2e2eea31 -r faa1129559b8 src/memory/memory.py --- a/src/memory/memory.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/memory/memory.py Sat Jan 24 01:00:29 2015 +0100 @@ -19,11 +19,14 @@ from sat.core.i18n import _ +from sat.core.log import getLogger +log = getLogger(__name__) + import os.path +import copy +from collections import namedtuple from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError from uuid import uuid4 -from sat.core.log import getLogger -log = getLogger(__name__) from twisted.internet import defer, reactor from twisted.words.protocols.jabber import jid from sat.core import exceptions @@ -36,6 +39,8 @@ from sat.tools import config as tools_config +PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses')) + class Sessions(object): """Sessions are data associated to key used for a temporary moment, with optional profile checking.""" DEFAULT_TIMEOUT = 600 @@ -212,6 +217,9 @@ self.host = host self._entities_cache = {} # XXX: keep presence/last resource/other data in cache # /!\ an entity is not necessarily in roster + # main key is bare jid, value is a dict + # where main key is resource, or None for bare jid + self._key_signals = set() # key which need a signal to frontends when updated self.subscriptions = {} self.auth_sessions = PasswordSessions() # remember the authenticated profiles self.disco = Discovery(host) @@ -228,6 +236,8 @@ d.addCallback(lambda ignore: self.memory_data.load()) d.chainDeferred(self.initialized) + ## Configuration ## + def parseMainConf(self): """look for main .ini configuration file, and parse it""" config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) @@ -265,6 +275,24 @@ log.error(_("Can't load parameters from file: %s") % e) return False + def save_xml(self, filename): + """Save parameters template to xml file + + @param filename (str): output file + @return: bool: True in case of success + """ + if not filename: + return False + #TODO: need to encrypt files (at least passwords !) and set permissions + filename = os.path.expanduser(filename) + try: + self.params.save_xml(filename) + log.debug(_("Parameters saved to file: %s") % filename) + return True + except Exception as e: + log.error(_("Can't save parameters to file: %s") % e) + return False + def load(self): """Load parameters and all memory things from db""" #parameters data @@ -275,6 +303,8 @@ @param profile: %(doc_profile)s""" return self.params.loadIndParams(profile) + ## Profiles/Sessions management ## + def startProfileSession(self, profile): """"Iniatialise session for a profile @param profile: %(doc_profile)s""" @@ -309,24 +339,6 @@ except KeyError: log.error(_("Trying to purge roster status cache for a profile not in memory: [%s]") % profile) - def save_xml(self, filename): - """Save parameters template to xml file - - @param filename (str): output file - @return: bool: True in case of success - """ - if not filename: - return False - #TODO: need to encrypt files (at least passwords !) and set permissions - filename = os.path.expanduser(filename) - try: - self.params.save_xml(filename) - log.debug(_("Parameters saved to file: %s") % filename) - return True - except Exception as e: - log.error(_("Can't save parameters to file: %s") % e) - return False - def getProfilesList(self): return self.storage.getProfilesList() @@ -365,6 +377,8 @@ self.auth_sessions.profileDelUnique(name) return self.params.asyncDeleteProfile(name, force) + ## History ## + def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile=C.PROF_KEY_NONE): assert profile != C.PROF_KEY_NONE if extra is None: @@ -393,163 +407,256 @@ return defer.succeed([]) return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile) - def _getLastResource(self, jid_s, profile_key): - jid_ = jid.JID(jid_s) - return self.getLastResource(jid_, profile_key) or "" - - def getLastResource(self, entity_jid, profile_key): - """Return the last resource used by an entity - @param entity_jid: entity jid - @param profile_key: %(doc_profile_key)s""" - data = self.getEntityData(entity_jid.userhostJID(), [C.ENTITY_LAST_RESOURCE], profile_key) - try: - return data[C.ENTITY_LAST_RESOURCE] - except KeyError: - return None + ## Statuses ## def _getPresenceStatuses(self, profile_key): ret = self.getPresenceStatuses(profile_key) return {entity.full():data for entity, data in ret.iteritems()} def getPresenceStatuses(self, profile_key): - """Get all the presence status of a profile + """Get all the presence statuses of a profile + @param profile_key: %(doc_profile_key)s @return: presence data: key=entity JID, value=presence data for this entity """ - profile = self.getProfileName(profile_key) - if not profile: - raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) + profile_cache = self._getProfileCache(profile_key) entities_presence = {} - for entity in self._entities_cache[profile]: - if "presence" in self._entities_cache[profile][entity]: - entities_presence[entity] = self._entities_cache[profile][entity]["presence"] - log.debug("Memory getPresenceStatus (%s)" % entities_presence) + for entity_jid, entity_data in profile_cache.iteritems(): + for resource, resource_data in entity_data.iteritems(): + full_jid = copy.copy(entity_jid) + full_jid.resource = resource + try: + presence_data = self.getEntityDatum(full_jid, "presence", profile_key) + except KeyError: + continue + entities_presence.setdefault(entity_jid, {})[resource or ''] = presence_data + return entities_presence - def isContactConnected(self, entity_jid, profile_key): - """Tell from the presence information if the given contact is connected. + def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key): + """Change the presence status of an entity + + @param entity_jid: jid.JID of the entity + @param show: show status + @param priority: priority + @param statuses: dictionary of statuses + @param profile_key: %(doc_profile_key)s + """ + presence_data = PresenceTuple(show, priority, statuses) + self.updateEntityData(entity_jid, "presence", presence_data, profile_key) + if entity_jid.resource and show != C.PRESENCE_UNAVAILABLE: + # If a resource is available, bare jid should not have presence information + try: + self.delEntityDatum(entity_jid.userhostJID(), "presence", profile_key) + except (KeyError, exceptions.UnknownEntityError): + pass + + ## Resources ## + + def _getAllResource(self, jid_s, profile_key): + jid_ = jid.JID(jid_s) + return self.getAllResources(jid_, profile_key) + + def getAllResources(self, entity_jid, profile_key): + """Return all resource from jid for which we have had data in this session + + @param entity_jid: bare jid of the entit + @param profile_key: %(doc_profile_key)s + return (list[unicode]): list of resources + + @raise exceptions.UnknownEntityError: if entity is not in cache + """ + if entity_jid.resource: + raise ValueError("getAllResources must be used with a bare jid (got {})".format(entity_jid)) + profile_cache = self._getProfileCache(profile_key) + try: + entity_data = profile_cache[entity_jid.userhostJID()] + except KeyError: + raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid)) + resources= set(entity_data.keys()) + resources.discard(None) + return resources + + def getAvailableResources(self, entity_jid, profile_key): + """Return available resource for entity_jid + + This method differs from getAllResources by returning only available resources + @param entity_jid: bare jid of the entit + @param profile_key: %(doc_profile_key)s + return (list[unicode]): list of available resources - @param entity_jid (JID): the entity to check + @raise exceptions.UnknownEntityError: if entity is not in cache + """ + available = [] + for resource in self.getAllResources(entity_jid, profile_key): + full_jid = copy.copy(entity_jid) + full_jid.resource = resource + try: + presence_data = self.getEntityDatum(full_jid, "presence", profile_key) + except KeyError: + log.debug("Can't get presence data for {}".format(full_jid)) + else: + if presence_data.show != C.PRESENCE_UNAVAILABLE: + available.append(resource) + return available + + def _getMainResource(self, jid_s, profile_key): + jid_ = jid.JID(jid_s) + return self.getMainResource(jid_, profile_key) or "" + + def getMainResource(self, entity_jid, profile_key): + """Return the main resource used by an entity + + @param entity_jid: bare entity jid @param profile_key: %(doc_profile_key)s - @return: boolean + @return (unicode): main resource or None + """ + if entity_jid.resource: + raise ValueError("getMainResource must be used with a bare jid (got {})".format(entity_jid)) + resources = self.getAllResources(entity_jid, profile_key) + priority_resources = [] + for resource in resources: + full_jid = copy.copy(entity_jid) + full_jid.resource = resource + try: + presence_data = self.getEntityDatum(full_jid, "presence", profile_key) + except KeyError: + log.debug("No presence information for {}".format(full_jid)) + continue + priority_resources.append((resource, presence_data.priority)) + try: + return max(priority_resources, key=lambda res_tuple: res_tuple[1])[0] + except ValueError: + log.warning("No resource found at all for {}".format(entity_jid)) + return None + + ## Entities data ## + + def _getProfileCache(self, profile_key): + """Check profile validity and return its cache + + @param profile: %(doc_profile_key)_s + @return (dict): profile cache + + @raise exceptions.ProfileUnknownError: if profile doesn't exist + @raise exceptions.ProfileNotInCacheError: if there is no cache for this profile """ profile = self.getProfileName(profile_key) if not profile: raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) try: - presence = self._entities_cache[profile][entity_jid]['presence'] - return len([True for status in presence.values() if status[0] != 'unavailable']) > 0 + profile_cache = self._entities_cache[profile] except KeyError: - return False + raise exceptions.ProfileNotInCacheError + return profile_cache - def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key): - """Change the presence status of an entity - @param entity_jid: jid.JID of the entity - @param show: show status - @param priotity: priotity - @param statuses: dictionary of statuses - @param profile_key: %(doc_profile_key)s + def setSignalOnUpdate(self, key, signal=True): + """Set a signal flag on the key + + When the key will be updated, a signal will be sent to frontends + @param key: key to signal + @param signal(boolean): if True, do the signal """ - profile = self.getProfileName(profile_key) - if not profile: - raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) - entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid] - resource = entity_jid.resource - if resource: - try: - type_ = self.getEntityDatum(entity_jid.userhostJID(), 'type', profile) - except KeyError: - type_ = 'contact' - if type_ != 'chatroom': - self.updateEntityData(entity_jid.userhostJID(), C.ENTITY_LAST_RESOURCE, resource, profile) - entity_data.setdefault("presence", {})[resource or ''] = (show, priority, statuses) + if signal: + self._key_signals.add(key) + else: + self._key_signals.discard(key) + + def getAllEntitiesIter(self, with_bare=False, profile_key=C.PROF_KEY_NONE): + """Return an iterator of full jids of all entities in cache - def _getEntitiesData(self, entity_jid, profile): - """Get data dictionary for entities - @param entity_jid: JID of the entity, or C.ENTITY_ALL for all entities) - @param profile: %(doc_profile)s - @return: entities_data (key=jid, value=entity_data) - @raise: exceptions.ProfileNotInCacheError if profile is not in cache + @param with_bare: if True, include bare jids + @param profile_key: %(doc_profile_key)s + @return (list[unicode]): list of jids """ - if not profile in self._entities_cache: - raise exceptions.ProfileNotInCacheError - if entity_jid == C.ENTITY_ALL: - entities_data = self._entities_cache[profile] - else: - entity_data = self._entities_cache[profile].setdefault(entity_jid, {}) - entities_data = {entity_jid: entity_data} - return entities_data - - def _updateEntityResources(self, entity_jid, profile): - """Add a known resource to bare entity_jid cache - @param entity_jid: full entity_jid (with resource) - @param profile: %(doc_profile)s - """ - assert(entity_jid.resource) - entity_data = self._getEntitiesData(entity_jid.userhostJID(), profile)[entity_jid.userhostJID()] - resources = entity_data.setdefault('resources', set()) - resources.add(entity_jid.resource) + profile_cache = self._getProfileCache(profile_key) + # we construct a list of all known full jids (bare jid of entities x resources) + for bare_jid, entity_data in profile_cache.iteritems(): + for resource in entity_data.iterkeys(): + if resource is None: + continue + full_jid = copy.copy(bare_jid) + full_jid.resource = resource + yield full_jid def updateEntityData(self, entity_jid, key, value, profile_key): """Set a misc data for an entity - @param entity_jid: JID of the entity, or C.ENTITY_ALL to update all entities) + + If key was registered with setSignalOnUpdate, a signal will be sent to frontends + @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities, + C.ENTITY_ALL for all entities (all resources + bare jids) @param key: key to set (eg: "type") @param value: value for this key (eg: "chatroom") @param profile_key: %(doc_profile_key)s """ - profile = self.getProfileName(profile_key) - if not profile: - raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) - entities_data = self._getEntitiesData(entity_jid, profile) - if entity_jid != C.ENTITY_ALL and entity_jid.resource: - self._updateEntityResources(entity_jid, profile) + profile_cache = self._getProfileCache(profile_key) + if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL): + entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key) + else: + entities = (entity_jid,) + + for jid_ in entities: + entity_data = profile_cache.setdefault(jid_.userhostJID(),{}).setdefault(jid_.resource, {}) - for jid_ in entities_data: - entity_data = entities_data[jid_] - if value == C.PROF_KEY_NONE and key in entity_data: - del entity_data[key] - else: - entity_data[key] = value - if isinstance(value, basestring): - self.host.bridge.entityDataUpdated(jid_.full(), key, value, profile) + entity_data[key] = value + if key in self._key_signals: + if not isinstance(value, basestring): + log.error(u"Setting a non string value ({}) for a key ({}) which has a signal flag".format(value, key)) + else: + self.host.bridge.entityDataUpdated(jid_.full(), key, value, self.getProfileName(profile_key)) - def delEntityData(self, entity_jid, key, profile_key): - """Delete data for an entity - @param entity_jid: JID of the entity, or C.ENTITY_ALL to delete data from all entities) + def delEntityDatum(self, entity_jid, key, profile_key): + """Delete a data for an entity + + @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities, + C.ENTITY_ALL for all entities (all resources + bare jids) @param key: key to delete (eg: "type") @param profile_key: %(doc_profile_key)s + + @raise exceptions.UnknownEntityError: if entity is not in cache + @raise KeyError: key is not in cache """ - entities_data = self._getEntitiesData(entity_jid, profile_key) - for entity_jid in entities_data: - entity_data = entities_data[entity_jid] + profile_cache = self._getProfileCache(profile_key) + if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL): + entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key) + else: + entities = (entity_jid,) + + for jid_ in entities: + try: + entity_data = profile_cache[jid_.userhostJID()][jid_.resource] + except KeyError: + raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(jid_)) try: del entity_data[key] - except KeyError: - log.debug("Key [%s] doesn't exist for [%s] in entities_cache" % (key, entity_jid.full())) + except KeyError as e: + if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL): + continue # we ignore KeyError when deleting keys from several entities + else: + raise e - def getEntityData(self, entity_jid, keys_list, profile_key): + def getEntityData(self, entity_jid, keys_list=None, profile_key=C.PROF_KEY_NONE): """Get a list of cached values for entity @param entity_jid: JID of the entity - @param keys_list (iterable): list of keys to get, empty list for everything + @param keys_list (iterable,None): list of keys to get, None for everything @param profile_key: %(doc_profile_key)s @return: dict withs values for each key in keys_list. if there is no value of a given key, resulting dict will have nothing with that key nether - @raise: exceptions.UnknownEntityError if entity is not in cache + + @raise exceptions.UnknownEntityError: if entity is not in cache """ - profile = self.getProfileName(profile_key) - if not profile: - raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) - entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid] - if not keys_list: + profile_cache = self._getProfileCache(profile_key) + try: + entity_data = profile_cache[entity_jid.userhostJID()][entity_jid.resource] + except KeyError: + raise exceptions.UnknownEntityError(u"Entity {} not in cache (was requesting {})".format(entity_jid, keys_list)) + if keys_list is None: return entity_data - ret = {} - for key in keys_list: - if key in entity_data: - ret[key] = entity_data[key] - return ret + + return {key: entity_data[key] for key in keys_list if key in entity_data} def getEntityDatum(self, entity_jid, key, profile_key): """Get a datum from entity @@ -559,34 +666,36 @@ @param profile_key: %(doc_profile_key)s @return: requested value - @raise: exceptions.UnknownEntityError if entity is not in cache - @raise: KeyError if there is no value for this key and this entity + @raise exceptions.UnknownEntityError: if entity is not in cache + @raise KeyError: if there is no value for this key and this entity """ return self.getEntityData(entity_jid, (key,), profile_key)[key] def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE): - """Remove cached data for entity + """Remove all cached data for entity + @param entity_jid: JID of the entity to delete - @param delete_all_resources: if True also delete all known resources form cache + @param delete_all_resources: if True also delete all known resources from cache (a bare jid must be given in this case) @param profile_key: %(doc_profile_key)s + + @raise exceptions.UnknownEntityError: if entity is not in cache """ - profile = self.getProfileName(profile_key) - if not profile: - raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) - to_delete = set([entity_jid]) + profile_cache = self._getProfileCache(profile_key) if delete_all_resources: if entity_jid.resource: raise ValueError(_("Need a bare jid to delete all resources")) - entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid] - resources = entity_data.setdefault('resources', set()) - to_delete.update([jid.JID("%s/%s" % (entity_jid.userhost(), resource)) for resource in resources]) + try: + del profile_cache[entity_jid] + except KeyError: + raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid)) + else: + try: + del profile_cache[entity_jid.userhostJID()][entity_jid.resource] + except KeyError: + raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid)) - for entity in to_delete: - try: - del self._entities_cache[profile][entity] - except KeyError: - log.warning(_("Can't delete entity [{}]: not in cache").format(entity.full())) + ## Encryption ## def encryptValue(self, value, profile): """Encrypt a value for the given profile. The personal key must be loaded @@ -644,6 +753,8 @@ d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load() return d.addCallback(gotIndMemory).addCallback(done) + ## Subscription requests ## + def addWaitingSub(self, type_, entity_jid, profile_key): """Called when a subcription request is received""" profile = self.getProfileName(profile_key) @@ -670,6 +781,8 @@ return self.subscriptions[profile] + ## Parameters ## + def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): return self.params.getStringParamA(name, category, attr, profile_key) @@ -705,3 +818,21 @@ def setDefault(self, name, category, callback, errback=None): return self.params.setDefault(name, category, callback, errback) + + ## Misc ## + + def isEntityAvailable(self, entity_jid, profile_key): + """Tell from the presence information if the given entity is available. + + @param entity_jid (JID): the entity to check (if bare jid is used, all resources are tested) + @param profile_key: %(doc_profile_key)s + @return (bool): True if entity is available + """ + if not entity_jid.resource: + return bool(self.getAvailableResources) # is any resource is available, entity is available + try: + presence_data = self.getEntityDatum(entity_jid, "presence", profile_key) + except KeyError: + log.debug("No presence information for {}".format(entity_jid)) + return False + return presence_data.show != C.PRESENCE_UNAVAILABLE diff -r 653f2e2eea31 -r faa1129559b8 src/memory/params.py --- a/src/memory/params.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/memory/params.py Sat Jan 24 01:00:29 2015 +0100 @@ -49,14 +49,14 @@ - + - - + + @@ -473,12 +473,12 @@ node = self._getParamNode(name, category) if not node: log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category}) - return defer.succeed(None) + raise ValueError("Requested param doesn't exist") if not self.checkSecurityLimit(node[1], security_limit): log.warning(_("Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!" % {'param': name, 'cat': category})) - return defer.succeed(None) + raise exceptions.PermissionError if node[0] == C.GENERAL: value = self._getParam(category, name, C.GENERAL) diff -r 653f2e2eea31 -r faa1129559b8 src/plugins/plugin_misc_text_commands.py --- a/src/plugins/plugin_misc_text_commands.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/plugins/plugin_misc_text_commands.py Sat Jan 24 01:00:29 2015 +0100 @@ -209,7 +209,7 @@ return False if not target_jid.resource: - target_jid.resource = self.host.memory.getLastResource(target_jid, profile) + target_jid.resource = self.host.memory.getMainResource(target_jid, profile) whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}] diff -r 653f2e2eea31 -r faa1129559b8 src/plugins/plugin_sec_otr.py --- a/src/plugins/plugin_sec_otr.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/plugins/plugin_sec_otr.py Sat Jan 24 01:00:29 2015 +0100 @@ -29,8 +29,9 @@ from twisted.words.protocols.jabber import jid from twisted.python import failure from twisted.internet import defer +from sat.memory import persistent import potr -from sat.memory import persistent +import copy NS_OTR = "otr_plugin" PRIVATE_KEY = "PRIVATE KEY" @@ -269,7 +270,7 @@ try: to_jid = jid.JID(menu_data['jid']) if not to_jid.resource: - to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored + to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored except KeyError: log.error(_("jid key is not present !")) return defer.fail(exceptions.DataError) @@ -287,7 +288,7 @@ try: to_jid = jid.JID(menu_data['jid']) if not to_jid.resource: - to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored + to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored except KeyError: log.error(_("jid key is not present !")) return defer.fail(exceptions.DataError) @@ -304,7 +305,7 @@ try: to_jid = jid.JID(menu_data['jid']) if not to_jid.resource: - to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored + to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored except KeyError: log.error(_("jid key is not present !")) return defer.fail(exceptions.DataError) @@ -374,7 +375,7 @@ try: to_jid = jid.JID(menu_data['jid']) if not to_jid.resource: - to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored + to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored except KeyError: log.error(_("jid key is not present !")) return defer.fail(exceptions.DataError) @@ -447,9 +448,9 @@ def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): if profile in self.skipped_profiles: return True - to_jid = mess_data['to'] + to_jid = copy.copy(mess_data['to']) if mess_data['type'] != 'groupchat' and not to_jid.resource: - to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed + to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed otrctx = self.context_managers[profile].getContextForUser(to_jid) if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT: if otrctx.state == potr.context.STATE_ENCRYPTED: @@ -475,7 +476,7 @@ if show != "unavailable": return True if not entity.resource: - entity.resource = self.host.memory.getLastResource(entity, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored + entity.resource = self.host.memory.getMainResource(entity, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored otrctx = self.context_managers[profile].getContextForUser(entity) otrctx.disconnect() return True diff -r 653f2e2eea31 -r faa1129559b8 src/plugins/plugin_xep_0054.py --- a/src/plugins/plugin_xep_0054.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/plugins/plugin_xep_0054.py Sat Jan 24 01:00:29 2015 +0100 @@ -85,6 +85,7 @@ host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile) host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True) host.trigger.add("presence_available", self.presenceTrigger) + host.memory.setSignalOnUpdate("avatar") def getHandler(self, profile): return XEP_0054_handler(self) diff -r 653f2e2eea31 -r faa1129559b8 src/plugins/plugin_xep_0060.py --- a/src/plugins/plugin_xep_0060.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/plugins/plugin_xep_0060.py Sat Jan 24 01:00:29 2015 +0100 @@ -55,7 +55,7 @@ OPT_PUBLISH_MODEL = 'pubsub#publish_model' def __init__(self, host): - log.info(_("PubSub plugin initialization")) + log.info(_(u"PubSub plugin initialization")) self.host = host self.managedNodes = [] self.clients = {} @@ -176,7 +176,7 @@ d_dict = {} for publisher, node in data.items(): if node not in found_nodes: - log.debug("Skip the items retrieval for [{node}]: node doesn't exist".format(node=node)) + log.debug(u"Skip the items retrieval for [{node}]: node doesn't exist".format(node=node)) continue # avoid pubsub "item-not-found" error d_dict[publisher] = client.items(service, node, max_items, item_ids, sub_id, client.parent.jid) defer.returnValue(d_dict) @@ -222,7 +222,7 @@ d_list = [] for nodeIdentifier in (set(nodeIdentifiers) - set(subscribed_nodes)): if nodeIdentifier not in found_nodes: - log.debug("Skip the subscription to [{node}]: node doesn't exist".format(node=nodeIdentifier)) + log.debug(u"Skip the subscription to [{node}]: node doesn't exist".format(node=nodeIdentifier)) continue # avoid sat-pubsub "SubscriptionExists" error d_list.append(client.subscribe(service, nodeIdentifier, sub_jid or client.parent.jid.userhostJID(), options=options)) defer.returnValue(d_list) @@ -318,7 +318,7 @@ def deleteReceived(self, event): #TODO: manage delete event - log.debug(_("Publish node deleted")) + log.debug(_(u"Publish node deleted")) # def purgeReceived(self, event): diff -r 653f2e2eea31 -r faa1129559b8 src/plugins/plugin_xep_0085.py --- a/src/plugins/plugin_xep_0085.py Sat Jan 24 00:15:01 2015 +0100 +++ b/src/plugins/plugin_xep_0085.py Sat Jan 24 01:00:29 2015 +0100 @@ -39,6 +39,7 @@ PARAM_KEY = "Notifications" PARAM_NAME = "Enable chat state notifications" ENTITY_KEY = PARAM_KEY + "_" + PARAM_NAME +DELETE_VALUE = "DELETE" PLUGIN_INFO = { "name": "Chat State Notifications Protocol Plugin", @@ -126,28 +127,31 @@ del self.map[profile] def updateEntityData(self, entity_jid, value, profile): - """ - Update the entity data of the given profile for one or all contacts. + """Update the entity data of the given profile for one or all contacts. + Reset the chat state(s) display if the notification has been disabled. - @param entity_jid: contact's JID, or '@ALL@' to update all contacts. - @param value: True, False or C.PROF_KEY_NONE to delete the entity data + @param entity_jid: contact's JID, or C.ENTITY_ALL to update all contacts. + @param value: True, False or DELETE_VALUE to delete the entity data @param profile: current profile """ - self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile) - if not value or value == C.PROF_KEY_NONE: - # disable chat state for this or these contact(s) - self.host.bridge.chatStateReceived(unicode(entity_jid), "", profile) + if value == DELETE_VALUE: + self.host.memory.delEntityData(entity_jid, ENTITY_KEY, profile) + else: + self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile) + if not value or value == DELETE_VALUE: + # reinit chat state UI for this or these contact(s) + self.host.bridge.chatStateReceived(entity_jid.full(), "", profile) def paramUpdateTrigger(self, name, value, category, type_, profile): - """ - Reset all the existing chat state entity data associated with this profile after a parameter modification. + """Reset all the existing chat state entity data associated with this profile after a parameter modification. + @param name: parameter name @param value: "true" to activate the notifications, or any other value to delete it @param category: parameter category @param type_: parameter type """ if (category, name) == (PARAM_KEY, PARAM_NAME): - self.updateEntityData("@ALL@", True if value == "true" else C.PROF_KEY_NONE, profile) + self.updateEntityData(C.ENTITY_ALL, True if bool("true") else DELETE_VALUE, profile) return False return True @@ -160,7 +164,7 @@ return True from_jid = JID(message.getAttribute("from")) - if self.__isMUC(from_jid, profile): + if self._isMUC(from_jid, profile): from_jid = from_jid.userhostJID() else: # update entity data for one2one chat # assert from_jid.resource # FIXME: assert doesn't work on normal message from server (e.g. server announce), because there is no resource @@ -200,27 +204,27 @@ def treatment(mess_data): message = mess_data['xml'] to_jid = JID(message.getAttribute("to")) - if not self.__checkActivation(to_jid, forceEntityData=True, profile=profile): + if not self._checkActivation(to_jid, forceEntityData=True, profile=profile): return mess_data try: # message with a body always mean active state domish.generateElementsNamed(message.elements(), name="body").next() message.addElement('active', NS_CHAT_STATES) # launch the chat state machine (init the timer) - if self.__isMUC(to_jid, profile): + if self._isMUC(to_jid, profile): to_jid = to_jid.userhostJID() self.__chatStateActive(to_jid, mess_data["type"], profile) except StopIteration: if "chat_state" in mess_data["extra"]: state = mess_data["extra"].pop("chat_state") - assert(state in CHAT_STATES) + assert state in CHAT_STATES message.addElement(state, NS_CHAT_STATES) return mess_data post_xml_treatments.addCallback(treatment) return True - def __isMUC(self, to_jid, profile): + def _isMUC(self, to_jid, profile): """Tell if that JID is a MUC or not @param to_jid (JID): full or bare JID to check @@ -229,13 +233,13 @@ """ try: type_ = self.host.memory.getEntityDatum(to_jid.userhostJID(), 'type', profile) - if type_ == 'chatroom': + if type_ == 'chatroom': # FIXME: should not use disco instead ? return True except (exceptions.UnknownEntityError, KeyError): pass return False - def __checkActivation(self, to_jid, forceEntityData, profile): + def _checkActivation(self, to_jid, forceEntityData, profile): """ @param to_jid: the contact's full JID (or bare if you know it's a MUC) @param forceEntityData: if set to True, a non-existing @@ -247,9 +251,10 @@ if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): return False # check if notifications should be sent to this contact - if self.__isMUC(to_jid, profile): + if self._isMUC(to_jid, profile): return True - assert to_jid.resource or not self.host.memory.isContactConnected(to_jid, profile) + # FIXME: this assertion crash when we want to send a message to an online bare jid + # assert to_jid.resource or not self.host.memory.isEntityAvailable(to_jid, profile) # must either have a resource, or talk to an offline contact try: return self.host.memory.getEntityDatum(to_jid, ENTITY_KEY, profile) except (exceptions.UnknownEntityError, KeyError): @@ -305,11 +310,11 @@ if profile is None: raise exceptions.ProfileUnknownError to_jid = JID(to_jid_s) - if self.__isMUC(to_jid, profile): + if self._isMUC(to_jid, profile): to_jid = to_jid.userhostJID() elif not to_jid.resource: - to_jid.resource = self.host.memory.getLastResource(to_jid, profile) - if not self.__checkActivation(to_jid, forceEntityData=False, profile=profile): + to_jid.resource = self.host.memory.getMainResource(to_jid, profile) + if not self._checkActivation(to_jid, forceEntityData=False, profile=profile): return try: self.map[profile][to_jid]._onEvent("composing") @@ -347,9 +352,9 @@ notification to the contact (the "active" state is automatically sent with each message) and set the timer. """ - assert(state in TRANSITIONS) + assert state in TRANSITIONS transition = TRANSITIONS[state] - assert("next_state" in transition and "delay" in transition) + assert "next_state" in transition and "delay" in transition if state != self.state and state != "active": if state != 'gone' or self.mess_type != 'groupchat':