diff src/plugins/plugin_xep_0054.py @ 223:86d249b6d9b7

Files reorganisation
author Goffi <goffi@goffi.org>
date Wed, 29 Dec 2010 01:06:29 +0100
parents plugins/plugin_xep_0054.py@f271fff3a713
children b1794cbb88e5
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/plugin_xep_0054.py	Wed Dec 29 01:06:29 2010 +0100
@@ -0,0 +1,246 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+SAT plugin for managing xep-0054
+Copyright (C) 2009, 2010  Jérôme Poisson (goffi@goffi.org)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from logging import debug, info, error
+from twisted.words.xish import domish
+from twisted.internet import protocol, defer, threads, reactor
+from twisted.words.protocols.jabber import client, jid, xmlstream
+from twisted.words.protocols.jabber import error as jab_error
+from twisted.words.protocols.jabber.xmlstream import IQ
+import os.path
+import pdb
+
+from zope.interface import implements
+
+from wokkel import disco, iwokkel
+
+from base64 import b64decode
+from hashlib import sha1
+from time import sleep
+
+try:
+    from twisted.words.protocols.xmlstream import XMPPHandler
+except ImportError:
+    from wokkel.subprotocols import XMPPHandler
+
+AVATAR_PATH = "/avatars"
+
+IQ_GET = '/iq[@type="get"]'
+NS_VCARD = 'vcard-temp'
+VCARD_REQUEST = IQ_GET + '/vCard[@xmlns="' + NS_VCARD + '"]'  #TODO: manage requests
+
+PRESENCE = '/presence'
+NS_VCARD_UPDATE = 'vcard-temp:x:update'
+VCARD_UPDATE = PRESENCE + '/x[@xmlns="' +  NS_VCARD_UPDATE + '"]'
+
+PLUGIN_INFO = {
+"name": "XEP 0054 Plugin",
+"import_name": "XEP_0054",
+"type": "XEP",
+"protocols": ["XEP-0054", "XEP-0153"],
+"dependencies": [],
+"main": "XEP_0054",
+"handler": "yes",
+"description": _("""Implementation of vcard-temp""")
+}
+
+class XEP_0054():
+
+    def __init__(self, host):
+        info(_("Plugin XEP_0054 initialization"))
+        self.host = host
+        self.avatar_path = os.path.expanduser(self.host.get_const('local_dir') + AVATAR_PATH)
+        self.vcard_cache = host.memory.getPrivate("vcard_cache") or {}  #used to store nicknames and avatar, key = jid
+        if not os.path.exists(self.avatar_path):
+            os.makedirs(self.avatar_path)
+        host.bridge.addMethod("getCard", ".communication", in_sign='ss', out_sign='s', method=self.getCard)
+        host.bridge.addMethod("getAvatarFile", ".communication", in_sign='s', out_sign='s', method=self.getAvatarFile)
+        host.bridge.addMethod("getCardCache", ".communication", in_sign='s', out_sign='a{ss}', method=self.getCardCache)
+
+    def getHandler(self, profile):
+        return XEP_0054_handler(self)  
+   
+    def update_cache(self, jid, name, value):
+        """update cache value
+        - save value in memory in case of change
+        - send updatedValue signal if the value is new or updated
+        """
+        if not self.vcard_cache.has_key(jid.userhost()):
+            self.vcard_cache[jid.userhost()] = {}
+        
+        cache = self.vcard_cache[jid.userhost()]
+        old_value = cache[name] if cache.has_key(name) else None
+        if not old_value or value != old_value:
+            cache[name] = value
+            self.host.memory.setPrivate("vcard_cache", self.vcard_cache)
+            self.host.bridge.updatedValue('card_'+name, {'jid':jid.userhost(), name:value})
+
+    def get_cache(self, jid, name):
+        """return cached value for jid
+        @param jid: target contact
+        @param name: name of the value ('nick' or 'avatar')
+        @return: wanted value or None"""
+        try:
+            return self.vcard_cache[jid.userhost()][name]
+        except KeyError:
+            return None
+
+
+    def save_photo(self, photo_xml):
+        """Parse a <PHOTO> elem and save the picture"""
+        for elem in photo_xml.elements():
+            if elem.name == 'TYPE':
+                info(_('Photo of type [%s] found') % str(elem))
+            if elem.name == 'BINVAL':
+                debug(_('Decoding binary'))
+                decoded = b64decode(str(elem))
+                hash = sha1(decoded).hexdigest()
+                filename = self.avatar_path+'/'+hash
+                if not os.path.exists(filename):
+                    with open(filename,'wb') as file:
+                        file.write(decoded)
+                    debug(_("file saved to %s") % hash)
+                else:
+                    debug(_("file [%s] already in cache") % hash)
+                return hash
+
+    @defer.deferredGenerator
+    def vCard2Dict(self, vcard, target):
+        """Convert a VCard to a dict, and save binaries"""
+        debug (_("parsing vcard"))
+        dictionary = {}
+        d = defer.Deferred()
+        
+        for elem in vcard.elements():
+            if elem.name == 'FN':
+                dictionary['fullname'] = unicode(elem)
+            elif elem.name == 'NICKNAME':
+                dictionary['nick'] = unicode(elem)
+                self.update_cache(target, 'nick', dictionary['nick'])
+            elif elem.name == 'URL':
+                dictionary['website'] = unicode(elem)
+            elif elem.name == 'EMAIL':
+                dictionary['email'] = unicode(elem)
+            elif elem.name == 'BDAY':
+                dictionary['birthday'] = unicode(elem) 
+            elif elem.name == 'PHOTO':
+                d2 = defer.waitForDeferred(
+                            threads.deferToThread(self.save_photo, elem))
+                yield d2
+                dictionary["avatar"] = d2.getResult()
+                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'])
+            else:
+                info (_('FIXME: [%s] VCard tag is not managed yet') % elem.name)
+
+        yield dictionary
+
+    def vcard_ok(self, answer):
+        """Called after the first get IQ"""
+        debug (_("VCard found"))
+
+        if answer.firstChildElement().name == "vCard":
+            d = self.vCard2Dict(answer.firstChildElement(), jid.JID(answer["from"]))
+            d.addCallback(lambda data: self.host.bridge.actionResult("RESULT", answer['id'], data))
+        else:
+            error (_("FIXME: vCard not found as first child element"))
+            self.host.bridge.actionResult("SUPPRESS", answer['id'], {}) #FIXME: maybe an error message would be best
+
+    def vcard_err(self, failure):
+        """Called when something is wrong with registration"""
+        error (_("Can't find VCard of %s") % failure.value.stanza['from'])
+        self.host.bridge.actionResult("SUPPRESS", failure.value.stanza['id'], {}) #FIXME: maybe an error message would be best
+  
+    def getCard(self, target, profile_key='@DEFAULT@'):
+        """Ask server for VCard
+        @param target: jid from which we want the VCard
+        @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'))
+            return ""
+        to_jid = jid.JID(target)
+        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()
+        query=reg_request.addElement('vCard', NS_VCARD)
+        reg_request.send(to_jid.userhost()).addCallbacks(self.vcard_ok, self.vcard_err)
+        return reg_request["id"] 
+
+    def getAvatarFile(self, hash):
+        """Give the full path of avatar from hash
+        @param hash: SHA1 hash
+        @return full_path
+        """
+        filename = self.avatar_path+'/'+hash
+        if not os.path.exists(filename):
+            error (_("Asking for an uncached avatar [%s]") %  hash)
+            return ""
+        return filename
+
+    def getCardCache(self, target):
+        """Request for cached values of profile 
+        return the cached nickname and avatar if exists, else get VCard
+        """
+        to_jid = jid.JID(target)
+        result = {}
+        nick = self.get_cache(to_jid, 'nick')
+        if nick:
+            result['nick'] = nick
+        avatar = self.get_cache(to_jid, 'avatar')
+        if avatar:
+            result['avatar'] = avatar
+        return result
+
+
+
+class XEP_0054_handler(XMPPHandler):
+    implements(iwokkel.IDisco)
+   
+    def __init__(self, plugin_parent):
+        self.plugin_parent = plugin_parent
+        self.host = plugin_parent.host
+
+    def connectionInitialized(self):
+        self.xmlstream.addObserver(VCARD_UPDATE, self.update)
+    
+    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
+        return [disco.DiscoFeature(NS_VCARD)]
+
+    def getDiscoItems(self, requestor, target, nodeIdentifier=''):
+        return []
+    
+    def update(self, presence):
+        """Request for VCard's nickname
+        return the cached nickname if exists, else get VCard
+        """
+        to_jid = jid.JID(presence['from'])
+        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(to_jid, 'avatar')
+                if not old_avatar or old_avatar != hash:
+                    debug(_('New avatar found, requesting vcard'))
+                    self.plugin_parent.getCard(to_jid.userhost(), self.parent.profile)