changeset 562:0bb2e0d1c878

core, plugin XEP-0054: avatar upload: - plugin XEP-0054: new setAvatar bridge method - new "presence_available" trigger - new DataError
author Goffi <goffi@goffi.org>
date Fri, 28 Dec 2012 01:00:31 +0100
parents 97f6a445d6e8
children da6eaa2af092
files frontends/src/bridge/DBus.py src/bridge/bridge_constructor/dbus_frontend_template.py src/core/exceptions.py src/core/xmpp.py src/plugins/plugin_xep_0054.py
diffstat 5 files changed, 94 insertions(+), 22 deletions(-) [+]
line wrap: on
line diff
--- 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)
 
--- 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)
 
--- 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
--- 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)
--- 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)