changeset 1262:f8a8434dbac7 frontends_multi_profiles

core: improved roster management + misc: - updated methods to no use anymore methods deprecated in Wokkel - use of full jid when it make sense instead of bare jid - getContacts, updateContact and delContact are now asynchronous
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 18:32:33 +0100
parents 93bce9e4c9c8
children cfd636203e8f
files src/bridge/DBus.py src/bridge/bridge_constructor/bridge_template.ini src/core/exceptions.py src/core/sat_main.py src/core/xmpp.py src/plugins/plugin_xep_0054.py src/stdui/ui_contact_list.py
diffstat 7 files changed, 102 insertions(+), 98 deletions(-) [+]
line wrap: on
line diff
--- a/src/bridge/DBus.py	Mon Nov 24 17:20:51 2014 +0100
+++ b/src/bridge/DBus.py	Wed Dec 10 18:32:33 2014 +0100
@@ -230,9 +230,9 @@
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='',
-                         async_callbacks=None)
-    def delContact(self, entity_jid, profile_key="@DEFAULT@"):
-        return self._callback("delContact", unicode(entity_jid), unicode(profile_key))
+                         async_callbacks=('callback', 'errback'))
+    def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("delContact", unicode(entity_jid), unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='(asa(sss))',
@@ -260,9 +260,9 @@
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='s', out_signature='a(sa{ss}as)',
-                         async_callbacks=None)
-    def getContacts(self, profile_key="@DEFAULT@"):
-        return self._callback("getContacts", unicode(profile_key))
+                         async_callbacks=('callback', 'errback'))
+    def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("getContacts", unicode(profile_key), callback=callback, errback=errback)
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ss', out_signature='as',
@@ -434,9 +434,9 @@
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='ssass', out_signature='',
-                         async_callbacks=None)
-    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
-        return self._callback("updateContact", unicode(entity_jid), unicode(name), groups, unicode(profile_key))
+                         async_callbacks=('callback', 'errback'))
+    def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None):
+        return self._callback("updateContact", unicode(entity_jid), unicode(name), groups, unicode(profile_key), callback=callback, errback=errback)
 
     def __attributes(self, in_sign):
         """Return arguments to user given a in_sign
--- a/src/bridge/bridge_constructor/bridge_template.ini	Mon Nov 24 17:20:51 2014 +0100
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Wed Dec 10 18:32:33 2014 +0100
@@ -255,12 +255,13 @@
 doc_param_0=%(doc_profile_key)s
 
 [getContacts]
+async=
 type=method
 category=core
 sig_in=s
 sig_out=a(sa{ss}as)
 param_0_default="@DEFAULT@"
-doc=Return information about all contacts
+doc=Return information about all contacts (the roster)
 doc_param_0=%(doc_profile_key)s
 doc_return=array of tuples with the following values:
  - JID of the contact
@@ -508,6 +509,7 @@
 doc_param_1=%(doc_profile_key)s
 
 [updateContact]
+async=
 type=method
 category=core
 sig_in=ssass
@@ -520,6 +522,7 @@
 doc_param_3=%(doc_profile_key)s
 
 [delContact]
+async=
 type=method
 category=core
 sig_in=ss
--- a/src/core/exceptions.py	Mon Nov 24 17:20:51 2014 +0100
+++ b/src/core/exceptions.py	Wed Dec 10 18:32:33 2014 +0100
@@ -93,5 +93,6 @@
 class PasswordError(Exception):
     pass
 
+
 class SkipHistory(Exception): # used in MessageReceivedTrigger to avoid history writting
     pass
--- a/src/core/sat_main.py	Mon Nov 24 17:20:51 2014 +0100
+++ b/src/core/sat_main.py	Wed Dec 10 18:32:33 2014 +0100
@@ -331,12 +331,15 @@
 
     def getContacts(self, profile_key):
         client = self.getClient(profile_key)
-        ret = []
-        for item in client.roster.getItems():  # we get all items for client's roster
-            # and convert them to expected format
-            attr = client.roster.getAttributes(item)
-            ret.append([item.jid.userhost(), attr, item.groups])
-        return ret
+        def got_roster(dummy):
+            ret = []
+            for item in client.roster.getItems():  # we get all items for client's roster
+                # and convert them to expected format
+                attr = client.roster.getAttributes(item)
+                ret.append([item.jid.userhost(), attr, item.groups])
+            return ret
+
+        return client.roster.got_roster.addCallback(got_roster)
 
     def getContactsFromGroup(self, group, profile_key):
         client = self.getClient(profile_key)
@@ -456,7 +459,7 @@
     def sendMessage(self, to_jid, msg, subject=None, mess_type='auto', extra={}, no_trigger=False, profile_key=C.PROF_KEY_NONE):
         #FIXME: check validity of recipient
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         client = self.profiles[profile]
         if extra is None:
             extra = {}
@@ -572,7 +575,7 @@
         if statuses is None:
             statuses = {}
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         priority = int(self.memory.getParamA("Priority", "Connection", profile_key=profile))
         self.profiles[profile].presence.available(to_jid, show, statuses, priority)
         #XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not broadcasted to generating resource)
@@ -588,7 +591,7 @@
         @param raw_jid: unicode entity's jid
         @param profile_key: profile"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         to_jid = jid.JID(raw_jid)
         log.debug(_('subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type': subs_type, 'jid': to_jid.full()})
         if subs_type == "subscribe":
@@ -606,8 +609,8 @@
     def addContact(self, to_jid, profile_key):
         """Add a contact in roster list"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
-        #self.profiles[profile].roster.addItem(to_jid)  #XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
+        assert profile
+        # presence is sufficient, as a roster push will be sent according to RFC 6121 §3.1.2
         self.profiles[profile].presence.subscribe(to_jid)
 
     def _updateContact(self, to_jid_s, name, groups, profile_key):
@@ -616,12 +619,12 @@
     def updateContact(self, to_jid, name, groups, profile_key):
         """update a contact in roster list"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
+        assert profile
         groups = set(groups)
         roster_item = RosterItem(to_jid)
         roster_item.name = name or None
         roster_item.groups = set(groups)
-        self.profiles[profile].roster.updateItem(roster_item)
+        return self.profiles[profile].roster.setItem(roster_item)
 
     def _delContact(self, to_jid_s, profile_key):
         return self.delContact(jid.JID(to_jid_s), profile_key)
@@ -629,10 +632,16 @@
     def delContact(self, to_jid, profile_key):
         """Remove contact from roster list"""
         profile = self.memory.getProfileName(profile_key)
-        assert(profile)
-        self.profiles[profile].roster.removeItem(to_jid)
-        self.profiles[profile].presence.unsubscribe(to_jid)
-
+        assert profile
+        d1 = self.profiles[profile].roster.removeItem(to_jid)
+        d2 = self.profiles[profile].presence.unsubscribe(to_jid)
+        d_list = defer.DefferedList([d1, d2])
+        def check_result(list_result):
+            for success, value in list_result:
+                if not success:
+                    raise value
+        d_list.addCallback(check_result)
+        return d_list
 
     ## Discovery ##
     # discovery methods are shortcuts to self.memory.disco
@@ -770,6 +779,9 @@
         @profile_key: %(doc_profile_key)s
         @return: a deferred which fire a dict where key can be:
             - xmlui: a XMLUI need to be displayed
+            - validated: if present, can be used to launch a callback, it can have the values
+                - C.BOOL_TRUE
+                - C.BOOL_FALSE
         """
         profile = self.memory.getProfileName(profile_key)
         if not profile:
--- a/src/core/xmpp.py	Mon Nov 24 17:20:51 2014 +0100
+++ b/src/core/xmpp.py	Wed Dec 10 18:32:33 2014 +0100
@@ -172,14 +172,42 @@
     def __init__(self, host):
         xmppim.RosterClientProtocol.__init__(self)
         self.host = host
-        self.got_roster = defer.Deferred()
+        self.got_roster = defer.Deferred() # called when roster is received and ready
         #XXX: the two following dicts keep a local copy of the roster
-        self._groups = {}  # map from groups to bare jids: key=group value=set of bare jids
-        self._jids = {}  # map from bare jids to RosterItem: key=jid value=RosterItem
+        self._groups = {}  # map from groups to jids: key=group value=set of jids
+        self._jids = None  # map from jids to RosterItem: key=jid value=RosterItem
 
     def rosterCb(self, roster):
-        for raw_jid, item in roster.iteritems():
-            self.onRosterSet(item)
+        assert roster is not None # FIXME: must be managed with roster versioning
+        self._jids = roster
+
+    def _registerItem(self, item):
+        """Register item in local cache
+
+        item must be already registered in self._jids before this method is called
+        @param item (RosterIem): item added
+        """
+        log.debug("registering item: {}".format(item.jid.full()))
+        if item.entity.resource:
+            log.warning("Received a roster item with a resource, this is not common but not restricted by RFC 6121, this case may be not well tested.")
+            import ipdb; ipdb.set_trace()
+        if not item.subscriptionTo and not item.subscriptionFrom and not item.ask:
+            #XXX: current behaviour: we don't want contact in our roster list
+            # if there is no presence subscription
+            # may change in the future
+            self.removeItem(item.jid) # FIXME: to be checked
+            return
+        if not item.subscriptionTo:
+            if not item.subscriptionFrom:
+                log.info(_("There's no subscription between you and [{}]!").format(item.jid.full()))
+            else:
+                log.info(_("You are not subscribed to [{}]!").format(item.jid.full()))
+        if not item.subscriptionFrom:
+            log.info(_("[{}] is not subscribed to you!").format(item.jid.full()))
+        #self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile)
+
+        for group in item.groups:
+            self._groups.setdefault(group, set()).add(item.entity)
 
     def requestRoster(self):
         """ ask the server for Roster list """
@@ -190,31 +218,9 @@
     def removeItem(self, to_jid):
         """Remove a contact from roster list
         @param to_jid: a JID instance
-        """
-        xmppim.RosterClientProtocol.removeItem(self, to_jid)
-        #TODO: check IQ result
-
-    #XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
-    #def addItem(self, to):
-        #"""Add a contact to roster list"""
-        #xmppim.RosterClientProtocol.addItem(self, to)
-        #TODO: check IQ result"""
-
-    def updateItem(self, roster_item):
+        @return: Deferred
         """
-        Update an item of the contact list.
-
-        @param roster_item: item to update
-        """
-        iq = compat.IQ(self.xmlstream, 'set')
-        iq.addElement((xmppim.NS_ROSTER, 'query'))
-        item = iq.query.addElement('item')
-        item['jid'] = roster_item.jid.userhost()
-        if roster_item.name:
-            item['name'] = roster_item.name
-        for group in roster_item.groups:
-            item.addElement('group', content=group)
-        return iq.send()
+        return xmppim.RosterClientProtocol.removeItem(self, to_jid)
 
     def getAttributes(self, item):
         """Return dictionary of attributes as used in bridge from a RosterItem
@@ -228,54 +234,34 @@
             item_attr['name'] = item.name
         return item_attr
 
-    def onRosterSet(self, item):
-        """Called when a new/update roster item is received"""
-        #TODO: send a signal to frontends
-        if not item.subscriptionTo and not item.subscriptionFrom and not item.ask:
-            #XXX: current behaviour: we don't want contact in our roster list
-            # if there is no presence subscription
-            # may change in the future
-            self.removeItem(item.jid)
-            return
-        log.debug(_("New contact in roster list: %s") % item.jid.full())
-        if not item.subscriptionTo:
-            if not item.subscriptionFrom:
-                log.info(_("There's no subscription between you and [%s]!") % item.jid.full())
-            else:
-                log.info(_("You are not subscribed to [%s]!") % item.jid.full())
-        if not item.subscriptionFrom:
-            log.info(_("[%s] is not subscribed to you!") % item.jid.full())
-        #self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile)
-
-        bare_jid = item.jid.userhostJID()
-        self._jids[bare_jid] = item
-        for group in item.groups:
-            self._groups.setdefault(group, set()).add(bare_jid)
+    def setReceived(self, request):
+        #TODO: implement roster versioning (cf RFC 6121 §2.6)
+        item = request.item
+        self._jids[item.entity] = item
         self.host.bridge.newContact(item.jid.full(), self.getAttributes(item), item.groups, self.parent.profile)
 
-    def onRosterRemove(self, entity):
-        """Called when a roster removal event is received"""
+    def removeReceived(self, request):
+        entity = request.item.entity
         print _("removing %s from roster list") % entity.full()
-        bare_jid = entity.userhostJID()
 
         # we first remove item from local cache (self._groups and self._jids)
         try:
-            item = self._jids.pop(bare_jid)
+            item = self._jids.pop(entity)
         except KeyError:
-            log.warning("Received a roster remove event for an item not in cache")
+            log.error("Received a roster remove event for an item not in cache ({})".format(entity))
             return
         for group in item.groups:
             try:
                 jids_set = self._groups[group]
-                jids_set.remove(bare_jid)
+                jids_set.remove(entity)
                 if not jids_set:
                     del self._groups[group]
             except KeyError:
                 log.warning("there is not cache for the group [%(groups)s] of the removed roster item [%(jid)s]" %
-                        {"group": group, "jid": bare_jid})
+                        {"group": group, "jid": entity})
 
         # then we send the bridge signal
-        self.host.bridge.contactDeleted(entity.userhost(), self.parent.profile)
+        self.host.bridge.contactDeleted(entity.full(), self.parent.profile)
 
     def getGroups(self):
         """Return a list of groups"""
@@ -283,17 +269,19 @@
 
     def getItem(self, jid):
         """Return RosterItem for a given jid
+
         @param jid: jid of the contact
-        @return: RosterItem or None if contact is not in cache"""
-        return self._jids.get(jid.userhostJID(), None)
+        @return: RosterItem or None if contact is not in cache
+        """
+        return self._jids.get(jid, None)
 
-    def getBareJids(self):
-        """Return all bare jids (as unicode) of the roster"""
+    def getJids(self):
+        """Return all jids of the roster"""
         return self._jids.keys()
 
     def isJidInRoster(self, entity_jid):
         """Return True if jid is in roster"""
-        return entity_jid.userhostJID() in self._jids
+        return entity_jid in self._jids
 
     def getItems(self):
         """Return all items of the roster"""
@@ -318,7 +306,7 @@
         super(SatPresenceProtocol, self).send(obj)
 
     def availableReceived(self, entity, show=None, statuses=None, priority=0):
-        log.debug(_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity': entity, 'show': show, 'statuses': statuses, 'priority': priority})
+        log.debug(_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity': entity, C.PRESENCE_SHOW: show, C.PRESENCE_STATUSES: statuses, C.PRESENCE_PRIORITY: priority})
 
         if not statuses:
             statuses = {}
@@ -341,7 +329,7 @@
                                         self.parent.profile)
 
     def unavailableReceived(self, entity, statuses=None):
-        log.debug(_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity': entity, 'statuses': statuses})
+        log.debug(_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity': entity, C.PRESENCE_STATUSES: statuses})
 
         if not statuses:
             statuses = {}
--- a/src/plugins/plugin_xep_0054.py	Mon Nov 24 17:20:51 2014 +0100
+++ b/src/plugins/plugin_xep_0054.py	Wed Dec 10 18:32:33 2014 +0100
@@ -102,9 +102,9 @@
         #       the current naive approach keeps a map between all jids of all profiles
         #       in persistent cache, and check if cached jid are in roster, then put avatar
         #       hashs in memory.
-        for _jid in client.roster.getBareJids() + [client.jid.userhost()]:
-            if _jid in self.avatars_cache:
-                self.host.memory.updateEntityData(jid.JID(_jid), "avatar", self.avatars_cache[_jid], client.profile)
+        for _jid in client.roster.getJids() + [client.jid]:
+            if _jid.userhost() in self.avatars_cache:
+                self.host.memory.updateEntityData(_jid, "avatar", self.avatars_cache[_jid.userhost()], client.profile)
 
     def profileConnected(self, profile):
         client = self.host.getClient(profile)
--- a/src/stdui/ui_contact_list.py	Mon Nov 24 17:20:51 2014 +0100
+++ b/src/stdui/ui_contact_list.py	Wed Dec 10 18:32:33 2014 +0100
@@ -50,7 +50,7 @@
         @return: list[string]
         """
         client = self.host.getClient(profile)
-        ret = [contact.userhost() for contact in client.roster.getBareJids()]
+        ret = [contact.full() for contact in client.roster.getJids()]
         ret.sort()
         return ret