# HG changeset patch # User Goffi # Date 1423514391 -3600 # Node ID bd69d341d9693a30eb4b4fea8183fd33c7329073 # Parent 8adcdf2cdfe1df7fd13a034cf1ab7fff797615de 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) diff -r 8adcdf2cdfe1 -r bd69d341d969 src/plugins/plugin_xep_0054.py --- 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 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 stanza with vcard data + + Check for avatar information, and get VCard if needed + @param presend(domish.Element): 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) +