Mercurial > libervia-backend
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)