Mercurial > libervia-backend
diff src/plugins/plugin_xep_0054.py @ 1367:f71a0fc26886
merged branch frontends_multi_profiles
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 18 Mar 2015 10:52:28 +0100 |
parents | 6dbeb2ef966c |
children | 069ad98b360d |
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0054.py Thu Feb 05 11:59:26 2015 +0100 +++ b/src/plugins/plugin_xep_0054.py Wed Mar 18 10:52:28 2015 +0100 @@ -36,7 +36,7 @@ from base64 import b64decode, b64encode from hashlib import sha1 from sat.core import exceptions -from sat.memory.persistent import PersistentDict +from sat.memory import persistent from PIL import Image from cStringIO import StringIO @@ -56,6 +56,8 @@ NS_VCARD_UPDATE = 'vcard-temp:x:update' VCARD_UPDATE = PRESENCE + '/x[@xmlns="' + NS_VCARD_UPDATE + '"]' +CACHED_DATA = {'avatar', 'nick'} + PLUGIN_INFO = { "name": "XEP 0054 Plugin", "import_name": "XEP-0054", @@ -79,67 +81,101 @@ 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.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.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) + host.memory.setSignalOnUpdate("avatar") + host.memory.setSignalOnUpdate("nick") def getHandler(self, profile): 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.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.cache[client.profile][client.jid.userhost()]['avatar']) presence_elt.addChild(x_elt) return True - def _fillCachedValues(self, result, client): + def _fillCachedValues(self, profile): #FIXME: this is really suboptimal, need to be reworked # 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) + # in persistent cache, then put avatar + # hashs in memory. Hashed should be shared between profiles + for jid_s, data in self.cache[profile].iteritems(): + jid_ = jid.JID(jid_s) + for name in CACHED_DATA: + try: + self.host.memory.updateEntityData(jid_, name, data[name], silent=True, profile_key=profile) + except KeyError: + pass - def profileConnected(self, profile): - client = self.host.getClient(profile) - client.roster.got_roster.addCallback(self._fillCachedValues, client) + @defer.inlineCallbacks + def profileConnecting(self, profile): + self.cache[profile] = persistent.PersistentBinaryDict(NS_VCARD, profile) + yield self.cache[profile].load() + self._fillCachedValues(profile) - def update_cache(self, jid, name, value, profile): + def profileDisconnected(self, profile): + log.debug(u"Deleting profile cache for avatars") + del self.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 + + save value in memory in case of change + @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 """ - try: - cached = self.host.memory.getEntityData(jid, [name], profile) - except exceptions.UnknownEntityError: - cached = {} - if not name in cached or cached[name] != value: - self.host.memory.updateEntityData(jid, name, value, profile) - if name == "avatar": - self.avatars_cache[jid.userhost()] = value + assert not jid_.resource # VCard are retrieved with bare jid + self.host.memory.updateEntityData(jid_, name, value, profile_key=profile) + if name in CACHED_DATA: + jid_s = jid_.userhost() + self.cache[profile].setdefault(jid_s, {})[name] = value + self.cache[profile].force(jid_s) - def get_cache(self, entity_jid, name, profile): + def getCache(self, entity_jid, name, profile): """return cached value for jid + @param entity_jid: target contact @param name: name of the value ('nick' or 'avatar') @param profile: %(doc_profile)s @return: wanted value or None""" + assert not entity_jid.resource # VCard are retrieved with bare jid try: data = self.host.memory.getEntityData(entity_jid, [name], profile) except exceptions.UnknownEntityError: return None return data.get(name) - def save_photo(self, photo_xml): + 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(): if elem.name == 'TYPE': @@ -148,13 +184,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 @@ -168,7 +198,7 @@ dictionary['fullname'] = unicode(elem) elif elem.name == 'NICKNAME': dictionary['nick'] = unicode(elem) - self.update_cache(target, 'nick', dictionary['nick'], profile) + self.updateCache(target, 'nick', dictionary['nick'], profile) elif elem.name == 'URL': dictionary['website'] = unicode(elem) elif elem.name == 'EMAIL': @@ -176,17 +206,24 @@ elif elem.name == 'BDAY': dictionary['birthday'] = unicode(elem) elif elem.name == 'PHOTO': - dictionary["avatar"] = yield threads.deferToThread(self.save_photo, elem) + dictionary["avatar"] = yield threads.deferToThread(self.savePhoto, elem) if not dictionary["avatar"]: # can happen in case of e.g. empty photo elem del dictionary['avatar'] else: - self.update_cache(target, 'avatar', dictionary['avatar'], profile) + self.updateCache(target, 'avatar', dictionary['avatar'], profile) else: log.info(_('FIXME: [%s] VCard tag is not managed yet') % elem.name) + # if a data in cache doesn't exist anymore, we need to reset it + # so we check CACHED_DATA no gotten (i.e. not in dictionary keys) + # and we reset them + for datum in CACHED_DATA.difference(dictionary.keys()): + log.debug(u"reseting vcard datum [{datum}] for {entity}".format(datum=datum, entity=target.full())) + self.updateCache(target, datum, '', profile) + defer.returnValue(dictionary) - def vcard_ok(self, answer, profile): + def _VCardCb(self, answer, profile): """Called after the first get IQ""" log.debug(_("VCard found")) @@ -202,30 +239,35 @@ log.error(_("FIXME: vCard not found as first child element")) self.host.bridge.actionResult("SUPPRESS", answer['id'], {}, profile) # FIXME: maybe an error message would be better - def vcard_err(self, failure, profile): + def _VCardEb(self, failure, profile): """Called when something is wrong with registration""" try: - log.error(_("Can't find VCard of %s") % failure.value.stanza['from']) + log.warning(_("Can't find VCard of %s") % failure.value.stanza['from']) self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}, profile) # FIXME: maybe an error message would be better + self.updateCache(jid.JID(failure.value.stanza['from']), "avatar", '', profile) except AttributeError: # 'ConnectionLost' object has no attribute 'stanza' - log.error(_("Can't find VCard: %s") % failure.getErrorMessage()) + 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: - log.error(_('Asking vcard for a non-existant or not connected profile')) - return "" + 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() reg_request["to"] = to_jid.userhost() reg_request.addElement('vCard', NS_VCARD) - reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err, callbackArgs=[profile], errbackArgs=[profile]) + reg_request.send(to_jid.userhost()).addCallbacks(self._VCardCb, self._VCardEb, callbackArgs=[profile], errbackArgs=[profile]) return reg_request["id"] def getAvatarFile(self, avatar_hash): @@ -266,6 +308,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): @@ -281,9 +324,8 @@ def elementBuilt(result): """Called once the image is at the right size/format, and the vcard set element is build""" set_avatar_elt, img_hash = result - self.avatars_cache[client.jid.userhost()] = img_hash # we need to update the hash, so we can send a new presence - # element with the right hash - return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) + self.updateCache(client.jid.userhostJID(), 'avatar', img_hash, client.profile) + return set_avatar_elt.send().addCallback(lambda ignore: client.presence.available()) # FIXME: should send the current presence, not always "available" ! d.addCallback(elementBuilt) @@ -307,16 +349,31 @@ 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 """ - from_jid = jid.JID(presence['from']) + # FIXME: doesn't manage MUC correctly + from_jid = jid.JID(presence['from']).userhostJID() #FIXME: wokkel's data_form should be used here 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) - old_avatar = self.plugin_parent.get_cache(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) + hash_ = str(elem) + old_avatar = self.plugin_parent.getCache(from_jid, 'avatar', 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: + 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) +