# HG changeset patch # User Goffi # Date 1356652831 -3600 # Node ID 0bb2e0d1c87807b7c1868490131bca047f34432d # Parent 97f6a445d6e88b24776f035344ccb6227db3964e core, plugin XEP-0054: avatar upload: - plugin XEP-0054: new setAvatar bridge method - new "presence_available" trigger - new DataError diff -r 97f6a445d6e8 -r 0bb2e0d1c878 frontends/src/bridge/DBus.py --- a/frontends/src/bridge/DBus.py Wed Dec 19 23:22:10 2012 +0100 +++ b/frontends/src/bridge/DBus.py Fri Dec 28 01:00:31 2012 +0100 @@ -226,6 +226,9 @@ def radiocolSongAdded(self, room_jid, song_path, profile): return self.db_plugin_iface.radiocolSongAdded(room_jid, song_path, profile) + def setAvatar(self, avatar_path, profile): + return self.db_plugin_iface.setAvatar(avatar_path, profile) + def sendFile(self, to, path, data, profile_key): return self.db_plugin_iface.sendFile(to, path, data, profile_key) diff -r 97f6a445d6e8 -r 0bb2e0d1c878 src/bridge/bridge_constructor/dbus_frontend_template.py --- a/src/bridge/bridge_constructor/dbus_frontend_template.py Wed Dec 19 23:22:10 2012 +0100 +++ b/src/bridge/bridge_constructor/dbus_frontend_template.py Fri Dec 28 01:00:31 2012 +0100 @@ -110,6 +110,9 @@ def radiocolSongAdded(self, room_jid, song_path, profile): return self.db_plugin_iface.radiocolSongAdded(room_jid, song_path, profile) + def setAvatar(self, avatar_path, profile): + return self.db_plugin_iface.setAvatar(avatar_path, profile) + def sendFile(self, to, path, data, profile_key): return self.db_plugin_iface.sendFile(to, path, data, profile_key) diff -r 97f6a445d6e8 -r 0bb2e0d1c878 src/core/exceptions.py --- a/src/core/exceptions.py Wed Dec 19 23:22:10 2012 +0100 +++ b/src/core/exceptions.py Fri Dec 28 01:00:31 2012 +0100 @@ -36,3 +36,6 @@ class NotFound(Exception): pass + +class DataError(Exception): + pass diff -r 97f6a445d6e8 -r 0bb2e0d1c878 src/core/xmpp.py --- a/src/core/xmpp.py Wed Dec 19 23:22:10 2012 +0100 +++ b/src/core/xmpp.py Fri Dec 28 01:00:31 2012 +0100 @@ -297,18 +297,22 @@ def available(self, entity=None, show=None, statuses=None, priority=0): - if not statuses: - statuses = {} - # default for us is None for wokkel - # so we must temporarily switch to wokkel's convention... - if 'default' in statuses: - statuses[None] = statuses['default'] + if not statuses: + statuses = {} + # default for us is None for wokkel + # so we must temporarily switch to wokkel's convention... + if 'default' in statuses: + statuses[None] = statuses['default'] - xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority) + xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority) + presence_elt = xmppim.AvailablePresence(entity, show, statuses, priority) + if not self.host.trigger.point("presence_available", presence_elt, self.parent): + return + self.send(presence_elt) - # ... before switching back - if None in statuses: - del statuses[None] + # ... before switching back + if None in statuses: + del statuses[None] def subscribed(self, entity): xmppim.PresenceClientProtocol.subscribed(self, entity) diff -r 97f6a445d6e8 -r 0bb2e0d1c878 src/plugins/plugin_xep_0054.py --- a/src/plugins/plugin_xep_0054.py Wed Dec 19 23:22:10 2012 +0100 +++ b/src/plugins/plugin_xep_0054.py Fri Dec 28 01:00:31 2012 +0100 @@ -24,16 +24,19 @@ from twisted.internet.defer import inlineCallbacks, returnValue from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber.xmlstream import IQ +from twisted.words.xish import domish import os.path from zope.interface import implements from wokkel import disco, iwokkel -from base64 import b64decode +from base64 import b64decode,b64encode from hashlib import sha1 from sat.core import exceptions from sat.memory.persistent import PersistentDict +import Image +from cStringIO import StringIO try: from twisted.words.protocols.xmlstream import XMPPHandler @@ -76,9 +79,19 @@ 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) 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) def getHandler(self, profile): - return XEP_0054_handler(self) + return XEP_0054_handler(self) + + def presenceTrigger(self, presence_elt, client): + if client.jid.userhost() in self.avatars_cache: + x_elt = domish.Element((NS_VCARD_UPDATE, 'x')) + x_elt.addElement('photo', content=self.avatars_cache[client.jid.userhost()]) + presence_elt.addChild(x_elt) + + return True def _fillCachedValues(self, result, client): #FIXME: this is really suboptimal, need to be reworked @@ -130,15 +143,15 @@ if elem.name == 'BINVAL': debug(_('Decoding binary')) decoded = b64decode(str(elem)) - hash = sha1(decoded).hexdigest() - filename = self.avatar_path+'/'+hash + 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) - debug(_("file saved to %s") % hash) + debug(_("file saved to %s") % image_hash) else: - debug(_("file [%s] already in cache") % hash) - return hash + debug(_("file [%s] already in cache") % image_hash) + return image_hash @inlineCallbacks def vCard2Dict(self, vcard, target, profile): @@ -174,7 +187,12 @@ debug (_("VCard found")) if answer.firstChildElement().name == "vCard": - d = self.vCard2Dict(answer.firstChildElement(), jid.JID(answer["from"]), profile) + _jid, steam = self.host.getJidNStream(profile) + try: + from_jid = jid.JID(answer["from"]) + except KeyError: + from_jid = _jid.userhostJID() + d = self.vCard2Dict(answer.firstChildElement(), from_jid, profile) d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data, profile)) else: error (_("FIXME: vCard not found as first child element")) @@ -191,7 +209,7 @@ @result: id to retrieve the profile""" current_jid, xmlstream = self.host.getJidNStream(profile_key) if not xmlstream: - error (_('Asking vcard for an non-existant or not connected profile')) + error (_('Asking vcard for a non-existant or not connected profile')) return "" profile = self.host.memory.getProfileName(profile_key) to_jid = jid.JID(target_s) @@ -203,17 +221,58 @@ reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err, callbackArgs=[profile], errbackArgs=[profile]) return reg_request["id"] - def getAvatarFile(self, hash): + def getAvatarFile(self, avatar_hash): """Give the full path of avatar from hash @param hash: SHA1 hash @return full_path """ - filename = self.avatar_path+'/'+hash + filename = self.avatar_path+'/'+avatar_hash if not os.path.exists(filename): - error (_("Asking for an uncached avatar [%s]") % hash) + error (_("Asking for an uncached avatar [%s]") % avatar_hash) return "" return filename + def _buildSetAvatar(self, vcard_set, filepath): + try: + img = Image.open(filepath) + except IOError: + raise exceptions.DataError("Can't open image") + + if img.size != (64, 64): + img.resize((64, 64)) + img_buf = StringIO() + img.save(img_buf, 'PNG') + + vcard_elt = vcard_set.addElement('vCard', NS_VCARD) + photo_elt = vcard_elt.addElement('PHOTO') + photo_elt.addElement('TYPE', content='image/png') + photo_elt.addElement('BINVAL', content=b64encode(img_buf.getvalue())) + img_hash = sha1(img_buf.getvalue()).hexdigest() + return (vcard_set, img_hash) + + def setAvatar(self, filepath, profile_key='@DEFAULT@'): + """Set avatar of the profile + @param filepath: path of the image of the avatar""" + #TODO: This is a temporary way of setting avatar, as other VCard informations are not managed. + # A proper full VCard management should be done (and more generaly a public/private profile) + client = self.host.getClient(profile_key) + if not client: + raise exceptions.NotConnectedProfileError(_('Trying to set avatar for a non-existant or not connected profile')) + + vcard_set = IQ(client.xmlstream,'set') + d = threads.deferToThread(self._buildSetAvatar, vcard_set, filepath) + + 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()) + + d.addCallback(elementBuilt) + + return d + class XEP_0054_handler(XMPPHandler): implements(iwokkel.IDisco)