# HG changeset patch # User Goffi # Date 1261362131 -39600 # Node ID 874de3020e1c92a54f473af2642ea4cdfd7e14e7 # Parent d24629c631fc18e0081eb68763b74d88c09ee0a9 Initial VCard (XEP-0054) support + misc fixes - new xep-0054 plugin, avatar are cached, new getProfile bridge method - gateways plugin (XEP-0100): new __private__ key in resulting data, used to keep target jid diff -r d24629c631fc -r 874de3020e1c frontends/sat_bridge_frontend/DBus.py --- a/frontends/sat_bridge_frontend/DBus.py Sat Dec 19 20:32:58 2009 +1100 +++ b/frontends/sat_bridge_frontend/DBus.py Mon Dec 21 13:22:11 2009 +1100 @@ -61,6 +61,9 @@ def findGateways(self, target): return self.db_comm_iface.findGateways(target) + def getProfile(self, target): + return self.db_comm_iface.getProfile(target) + def in_band_register(self, target): return self.db_comm_iface.in_band_register(target) diff -r d24629c631fc -r 874de3020e1c frontends/wix/main_window.py --- a/frontends/wix/main_window.py Sat Dec 19 20:32:58 2009 +1100 +++ b/frontends/wix/main_window.py Mon Dec 21 13:22:11 2009 +1100 @@ -25,6 +25,7 @@ from param import Param from form import Form from gateways import GatewaysManager +from profile import Profile import gobject import os.path import pdb @@ -43,7 +44,8 @@ idPARAM = 4 idADD_CONTACT = 5 idREMOVE_CONTACT = 6 -idFIND_GATEWAYS = 7 +idSHOW_PROFILE = 7 +idFIND_GATEWAYS = 8 const_DEFAULT_GROUP = "Unclassed" const_STATUS = {"Online":"", "Want to discuss":"chat", @@ -208,6 +210,8 @@ contactMenu = wx.Menu() contactMenu.Append(idADD_CONTACT, "&Add contact"," Add a contact to your list") contactMenu.Append(idREMOVE_CONTACT, "&Remove contact"," Remove the selected contact from your list") + contactMenu.AppendSeparator() + contactMenu.Append(idSHOW_PROFILE, "&Show profile", " Show contact's profile") communicationMenu = wx.Menu() communicationMenu.Append(idFIND_GATEWAYS, "&Find Gateways"," Find gateways to legacy IM") menuBar = wx.MenuBar() @@ -223,6 +227,7 @@ wx.EVT_MENU(self, idEXIT, self.onExit) wx.EVT_MENU(self, idADD_CONTACT, self.onAddContact) wx.EVT_MENU(self, idREMOVE_CONTACT, self.onRemoveContact) + wx.EVT_MENU(self, idSHOW_PROFILE, self.onShowProfile) wx.EVT_MENU(self, idFIND_GATEWAYS, self.onFindGateways) @@ -332,12 +337,18 @@ self.current_action_ids.remove(id) debug ("Form received") form=Form(self, title='Registration', target = data['target'], type = data['type'], xml_data = data['xml']) + elif type == "RESULT": + self.current_action_ids.remove(id) + if self.current_action_ids_cb.has_key(id): + callback = self.current_action_ids_cb[id] + del self.current_action_ids_cb[id] + callback(data) elif type == "DICT_DICT": self.current_action_ids.remove(id) if self.current_action_ids_cb.has_key(id): callback = self.current_action_ids_cb[id] del self.current_action_ids_cb[id] - callback(id,data) + callback(data) print ("Dict of dict found as result") else: error ("FIXME FIXME FIXME: type [%s] not implemented" % type) @@ -428,7 +439,7 @@ dlg.Destroy() return - dlg = wx.MessageDialog(self, "Are you sure you want to delete %s from your roster list ?" % target.short, + dlg = wx.MessageDialog(self, "Are you sure you want to delete %s from your roster list ?" % target.short, 'Contact suppression', wx.YES_NO | wx.ICON_QUESTION ) @@ -439,6 +450,27 @@ dlg.Destroy() + def onShowProfile(self, e): + debug("Show contact's profile request") + target = self.contactList.getSelection() + if not target: + dlg = wx.MessageDialog(self, "You haven't selected any contact !", + 'Error', + wx.OK | wx.ICON_ERROR + ) + dlg.ShowModal() + dlg.Destroy() + return + id = self.bridge.getProfile(target.short) + self.current_action_ids.add(id) + self.current_action_ids_cb[id] = self.onProfileReceived + + def onProfileReceived(self, data): + """Called when a profile is received""" + debug ('Profile received: [%s]' % data) + profile=Profile(self, data) + + def onFindGateways(self, e): debug("Find Gateways request") id = self.bridge.findGateways(self.whoami.domain) @@ -446,9 +478,11 @@ self.current_action_ids_cb[id] = self.onGatewaysFound print "Find Gateways id=", id - def onGatewaysFound(self, id, data): + def onGatewaysFound(self, data): """Called when SàT has found the server gateways""" - gatewayManager = GatewaysManager(self, data) + target = data['__private__']['target'] + del data['__private__'] + gatewayManager = GatewaysManager(self, data, server=target) def onClose(self, e): info("Exiting...") diff -r d24629c631fc -r 874de3020e1c frontends/wix/profile.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/wix/profile.py Mon Dec 21 13:22:11 2009 +1100 @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +wix: a SAT frontend +Copyright (C) 2009 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 . +""" + +import wx +import pdb +from logging import debug, info, error +from tools.jid import JID + + +class Profile(wx.Frame): + """This class is used to show/modify profile given by SàT""" + + def __init__(self, host, data, title="Profile"): + super(Profile, self).__init__(None, title=title) + self.host = host + + self.name_dict = { 'fullname': 'Full Name', + 'nick' : 'Nickname', + 'birthday' : 'Birthday', + 'phone' : 'Phone #', + 'website' : 'Website', + 'email' : 'E-mail' + } + self.ctl_list = {} # usefull to access ctrl, key = (name) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.notebook=wx.Notebook(self, -1) + self.sizer.Add(self.notebook, 1, flag=wx.EXPAND) + self.SetSizer(self.sizer) + self.SetAutoLayout(True) + + #events + self.Bind(wx.EVT_CLOSE, self.onClose, self) + + self.MakeModal() + self.showData(data) + self.Show() + + def showData(self, data): + flags = wx.TE_READONLY + + #General tab + generaltab = wx.Panel(self.notebook) + sizer = wx.FlexGridSizer(cols=2) + sizer.AddGrowableCol(1) + generaltab.SetSizer(sizer) + generaltab.SetAutoLayout(True) + for field in ['fullname','nick', 'birthday', 'phone', 'website', 'email']: + value = data[field] if data.has_key(field) else '' + label=wx.StaticText(generaltab, -1, self.name_dict[field]+": ") + sizer.Add(label) + self.ctl_list[field] = wx.TextCtrl(generaltab, -1, value, style = flags) + sizer.Add(self.ctl_list[field], 1, flag = wx.EXPAND) + + + self.notebook.AddPage(generaltab, "General") + + + def onClose(self, event): + """Close event""" + debug("close") + self.MakeModal(False) + event.Skip() + diff -r d24629c631fc -r 874de3020e1c plugins/plugin_xep_0054.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/plugin_xep_0054.py Mon Dec 21 13:22:11 2009 +1100 @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +SAT plugin for managing xep-0054 +Copyright (C) 2009 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 . +""" + +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 + +AVATAR_PATH = "/avatars" + +IQ_GET = '/iq[@type="get"]' +NS_VCARD = 'vcard-temp' +VCARD_REQUEST = IQ_GET + '/si[@xmlns="' + NS_VCARD + '"]' #TODO: manage requests + +PLUGIN_INFO = { +"name": "XEP 0054 Plugin", +"import_name": "XEP_0054", +"type": "XEP", +"dependencies": [], +"main": "XEP_0054", +"description": """Implementation of vcard-temp""" +} + +class XEP_0054(): + implements(iwokkel.IDisco) + + 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) + if not os.path.exists(self.avatar_path): + os.makedirs(self.avatar_path) + host.bridge.addMethod("getProfile", ".communication", in_sign='s', out_sign='s', method=self.getProfile) + + def getDiscoInfo(self, requestor, target, nodeIdentifier=''): + return [disco.DiscoFeature(NS_VCARD)] + + def getDiscoItems(self, requestor, target, nodeIdentifier=''): + return [] + + def save_photo(self, photo_xml): + """Parse a elem and save the picture""" + print "save_photo result" + 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): + """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) + 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': + debug('photo deferred') + d2 = defer.waitForDeferred( + threads.deferToThread(self.save_photo, elem)) + yield d2 + dictionary["photo"] = d2.getResult() + 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()) + 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 getProfile(self, target): + """Ask server for VCard + @param target: jid from which we want the VCard + @result: id to retrieve the profile""" + to_jid = jid.JID(target) + debug("Asking for %s's VCard" % to_jid.full()) + reg_request=IQ(self.host.xmlstream,'get') + reg_request["from"]=self.host.me.full() + reg_request["to"] = to_jid.full() + query=reg_request.addElement('vCard', NS_VCARD) + reg_request.send(to_jid.full()).addCallbacks(self.vcard_ok, self.vcard_err) + return reg_request["id"] + diff -r d24629c631fc -r 874de3020e1c plugins/plugin_xep_0100.py --- a/plugins/plugin_xep_0100.py Sat Dec 19 20:32:58 2009 +1100 +++ b/plugins/plugin_xep_0100.py Mon Dec 21 13:22:11 2009 +1100 @@ -75,15 +75,17 @@ self.__inc_handled_items(request_id) - def discoItems(self, disco, request_id): + def discoItems(self, disco, request_id, target): """Look for items with disco protocol, and ask infos for each one""" + #FIXME: target is used as we can't find the original iq node (parent is None) + # an other way would avoid this useless parameter (is there a way with wokkel ?) if len(disco._items) == 0: debug ("No gateway found") self.host.actionResultExt(request_id,"DICT_DICT",{}) return - self.__gateways[request_id] = {'__total_items':len(disco._items), '__handled_items':0} + self.__gateways[request_id] = {'__total_items':len(disco._items), '__handled_items':0, '__private__':{'target':target.full()}} for item in disco._items: debug ("item found: %s", item.name) self.host.disco.requestInfo(item.entity).addCallback(self.discoInfo, entity=item.entity, request_id=request_id) @@ -105,7 +107,8 @@ """Find gateways in the target JID, using discovery protocol Return an id used for retrieving the list of gateways """ - debug ("find gateways (target = %s)" % target) + to_jid = jid.JID(target) + debug ("find gateways (target = %s)" % to_jid.full()) request_id = self.host.get_next_id() - self.host.disco.requestItems(jid.JID(target)).addCallback(self.discoItems, request_id=request_id) + self.host.disco.requestItems(to_jid).addCallback(self.discoItems, request_id=request_id, target = to_jid) return request_id