changeset 1317:bd69d341d969 frontends_multi_profiles

plugin xep-0054: various improvments on avatars management: - avatars_cache is now managing cache load per profile - use of new profileConnecting method to load persistent data, this prevent to have presence_available trigger called before cache data is loaded - better management of avatars already in cache (prevent decoding avatars when it is already in cache)
author Goffi <goffi@goffi.org>
date Mon, 09 Feb 2015 21:39:51 +0100
parents 8adcdf2cdfe1
children 6c7d89843f1b
files src/plugins/plugin_xep_0054.py
diffstat 1 files changed, 69 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0054.py	Mon Feb 09 21:39:51 2015 +0100
+++ b/src/plugins/plugin_xep_0054.py	Mon Feb 09 21:39:51 2015 +0100
@@ -79,9 +79,8 @@
         self.avatar_path = os.path.join(self.host.memory.getConfig('', 'local_dir'), AVATAR_PATH)
         if not os.path.exists(self.avatar_path):
             os.makedirs(self.avatar_path)
-        self.avatars_cache = PersistentDict(NS_VCARD)
-        self.initialised = self.avatars_cache.load()  # FIXME: resulting deferred must be correctly managed
-        host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self.getCard)
+        self.avatars_cache = {}
+        host.bridge.addMethod("getCard", ".plugin", in_sign='ss', out_sign='s', method=self._getCard)
         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)
@@ -91,9 +90,9 @@
         return XEP_0054_handler(self)
 
     def presenceTrigger(self, presence_elt, client):
-        if client.jid.userhost() in self.avatars_cache:
+        if client.jid.userhost() in self.avatars_cache[client.profile]:
             x_elt = domish.Element((NS_VCARD_UPDATE, 'x'))
-            x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()])
+            x_elt.addElement('photo', content=self.avatars_cache[client.profile][client.jid.userhost()])
             presence_elt.addChild(x_elt)
 
         return True
@@ -103,28 +102,33 @@
         #       the current naive approach keeps a map between all jids of all profiles
         #       in persistent cache, then put avatar
         #       hashs in memory. Hashed should be shared between profiles
-        for jid_s, avatar_hash in self.avatars_cache.iteritems():
+        for jid_s, avatar_hash in self.avatars_cache[profile].iteritems():
             jid_ = jid.JID(jid_s)
             self.host.memory.updateEntityData(jid_, "avatar", avatar_hash, silent=True, profile_key=profile)
 
     @defer.inlineCallbacks
-    def profileConnected(self, profile):
-        yield self.initialised
+    def profileConnecting(self, profile):
+        self.avatars_cache[profile] = PersistentDict(NS_VCARD, profile)
+        yield self.avatars_cache[profile].load()
         self._fillCachedValues(profile)
 
+    def profileDisconnected(self, profile):
+        log.debug(u"Deleting profile cache for avatars")
+        del self.avatars_cache[profile]
+
     def updateCache(self, jid_, name, value, profile):
         """update cache value
 
         save value in memory in case of change
-        @param jid_: jid of the owner of the vcard
-        @param name: name of the item which changed
-        @param value: new value of the item
-        @param profile: profile which received the update
+        @param jid_(jid.JID): jid of the owner of the vcard
+        @param name(str): name of the item which changed
+        @param value(unicode): new value of the item
+        @param profile(unicode): profile which received the update
         """
         assert not jid_.resource # VCard are retrieved with bare jid
         self.host.memory.updateEntityData(jid_, name, value, profile_key=profile)
         if name == "avatar":
-            self.avatars_cache[jid_.userhost()] = value
+            self.avatars_cache[profile][jid_.userhost()] = value
 
     def getCache(self, entity_jid, name, profile):
         """return cached value for jid
@@ -140,6 +144,28 @@
             return None
         return data.get(name)
 
+    def _getFilename(self, hash_):
+        """Get filename from hash
+
+        @param hash_: hash of the avatar
+        @return (str): absolute filename of the avatar
+        """
+        return os.path.join(self.avatar_path, hash_)
+
+    def saveAvatarFile(self, data, hash_):
+        """Save the avatar picture if it doesn't already exists
+
+        @param data(str): binary image of the avatar
+        @param hash_(str): hash of the binary data (will be used for the filename)
+        """
+        filename = self._getFilename(hash_)
+        if not os.path.exists(filename):
+            with open(filename, 'wb') as file_:
+                file_.write(data)
+            log.debug(_("file saved to %s") % hash_)
+        else:
+            log.debug(_("file [%s] already in cache") % hash_)
+
     def savePhoto(self, photo_xml):
         """Parse a <PHOTO> elem and save the picture"""
         for elem in photo_xml.elements():
@@ -149,13 +175,7 @@
                 log.debug(_('Decoding binary'))
                 decoded = b64decode(str(elem))
                 image_hash = sha1(decoded).hexdigest()
-                filename = self.avatar_path + '/' + image_hash
-                if not os.path.exists(filename):
-                    with open(filename, 'wb') as file_:
-                        file_.write(decoded)
-                    log.debug(_("file saved to %s") % image_hash)
-                else:
-                    log.debug(_("file [%s] already in cache") % image_hash)
+                self.saveAvatarFile(decoded, image_hash)
                 return image_hash
 
     @defer.inlineCallbacks
@@ -212,15 +232,20 @@
         except AttributeError:  # 'ConnectionLost' object has no attribute 'stanza'
             log.warning(_("Can't find VCard: %s") % failure.getErrorMessage())
 
-    def getCard(self, target_s, profile_key=C.PROF_KEY_NONE):
+    def _getCard(self, target_s, profile_key=C.PROF_KEY_NONE):
+        return self.getCard(jid.JID(target_s), profile_key)
+
+    def getCard(self, target, profile_key=C.PROF_KEY_NONE):
         """Ask server for VCard
-        @param target_s: jid from which we want the VCard
-        @result: id to retrieve the profile"""
+
+        @param target(jid.JID): jid from which we want the VCard
+        @result: id to retrieve the profile
+        """
         current_jid, xmlstream = self.host.getJidNStream(profile_key)
         if not xmlstream:
             raise exceptions.ProfileUnknownError('Asking vcard for a non-existant or not connected profile ({})'.format(profile_key))
         profile = self.host.memory.getProfileName(profile_key)
-        to_jid = jid.JID(target_s)
+        to_jid = target.userhostJID()
         log.debug(_("Asking for %s's VCard") % to_jid.userhost())
         reg_request = IQ(xmlstream, 'get')
         reg_request["from"] = current_jid.full()
@@ -267,6 +292,7 @@
         photo_elt.addElement('TYPE', content='image/png')
         photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue()))
         img_hash = sha1(img_buf.getvalue()).hexdigest()
+        self.saveAvatarFile(img_buf.getvalue(), img_hash)
         return (vcard_set, img_hash)
 
     def setAvatar(self, filepath, profile_key=C.PROF_KEY_NONE):
@@ -307,8 +333,10 @@
         return []
 
     def update(self, presence):
-        """Request for VCard's nickname
-        return the cached nickname if exists, else get VCard
+        """Called on <presence/> stanza with vcard data
+
+        Check for avatar information, and get VCard if needed
+        @param presend(domish.Element): <presence/> stanza
         """
         # FIXME: doesn't manage MUC correctly
         from_jid = jid.JID(presence['from']).userhostJID()
@@ -316,10 +344,20 @@
         x_elem = filter(lambda x: x.name == "x", presence.elements())[0]  # We only want the "x" element
         for elem in x_elem.elements():
             if elem.name == 'photo':
-                _hash = str(elem)
+                hash_ = str(elem)
                 old_avatar = self.plugin_parent.getCache(from_jid, 'avatar', self.parent.profile)
-                if not old_avatar or old_avatar != _hash:
-                    log.debug(_('New avatar found, requesting vcard'))
-                    self.plugin_parent.getCard(from_jid.userhost(), self.parent.profile)
+                filename = self.plugin_parent._getFilename(hash_)
+                if not old_avatar or old_avatar != hash_:
+                    if os.path.exists(filename):
+                        log.debug(u"New avatar found for [{}], it's already in cache, we use it".format(from_jid.full()))
+                        self.plugin_parent.updateCache(from_jid, 'avatar', hash_, self.parent.profile)
+                    else:
+                        log.debug(u'New avatar found for [{}], requesting vcard'.format(from_jid.full()))
+                        self.plugin_parent.getCard(from_jid, self.parent.profile)
                 else:
-                    log.debug("avatar for {} already in cache".format(from_jid))
+                    if os.path.exists(filename):
+                        log.debug(u"avatar for {} already in cache".format(from_jid.full()))
+                    else:
+                        log.error(u"Avatar for [{}] should be in cache but it is not ! We get it".format(from_jid.full()))
+                        self.plugin_parent.getCard(from_jid, self.parent.profile)
+