# HG changeset patch # User Goffi # Date 1306179148 -7200 # Node ID 0a8eb0461f319f918e6aea39eb1426bf56d89e53 # Parent 608a4a2ba94e5412228e9aaca47c34851627a9d0 core: main SAT class now moved in its own module core.sat_main diff -r 608a4a2ba94e -r 0a8eb0461f31 src/core/sat_main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/sat_main.py Mon May 23 21:32:28 2011 +0200 @@ -0,0 +1,613 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +SAT: a jabber client +Copyright (C) 2009, 2010, 2011 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 . +""" + +CONST = { + 'client_name' : u'SàT (Salut à toi)', + 'client_version' : u'0.1.1D', #Please add 'D' at the end for dev versions + 'local_dir' : '~/.sat' +} + +from twisted.application import service +from twisted.internet import defer + +from twisted.words.protocols.jabber import jid, xmlstream +from twisted.words.xish import domish + +from twisted.internet import reactor + +from wokkel import compat + +from sat.bridge.DBus import DBusBridge +import logging +from logging import debug, info, error + +import sys +import os.path + +from sat.core.xmpp import SatXMPPClient, SatMessageProtocol, SatRosterProtocol, SatPresenceProtocol, SatDiscoProtocol, SatFallbackHandler, RegisteringAuthenticator, SatVersionHandler +from sat.tools.memory import Memory +from sat.tools.xml_tools import tupleList2dataForm +from sat.tools.misc import TriggerManager +from glob import glob + +try: + from twisted.words.protocols.xmlstream import XMPPHandler +except ImportError: + from wokkel.subprotocols import XMPPHandler + + +### logging configuration FIXME: put this elsewhere ### +logging.basicConfig(level=logging.DEBUG, + format='%(message)s') +### + + +sat_id = 0 + +def sat_next_id(): + global sat_id + sat_id+=1 + return "sat_id_"+str(sat_id) + +class SAT(service.Service): + + def get_next_id(self): + return sat_next_id() + + def get_const(self, name): + """Return a constant""" + if not CONST.has_key(name): + error(_('Trying to access an undefined constant')) + raise Exception + return CONST[name] + + def set_const(self, name, value): + """Save a constant""" + if CONST.has_key(name): + error(_('Trying to redefine a constant')) + raise Exception + CONST[name] = value + + def __init__(self): + #TODO: standardize callback system + + local_dir = os.path.expanduser(self.get_const('local_dir')) + if not os.path.exists(local_dir): + os.makedirs(local_dir) + + self.__waiting_conf = {} #callback called when a confirmation is received + self.__progress_cb_map = {} #callback called when a progress is requested (key = progress id) + self.__general_cb_map = {} #callback called for general reasons (key = name) + self.__private_data = {} #used for internal callbacks (key = id) + self.trigger = TriggerManager() #trigger are user to change SàT behaviour + self.profiles = {} + self.plugins = {} + self.menus = {} #used to know which new menus are wanted by plugins + + self.memory=Memory(self) + + self.bridge=DBusBridge() + self.bridge.register("getVersion", lambda: self.get_const('client_version')) + self.bridge.register("getProfileName", self.memory.getProfileName) + self.bridge.register("getProfilesList", self.memory.getProfilesList) + self.bridge.register("createProfile", self.memory.createProfile) + self.bridge.register("deleteProfile", self.memory.deleteProfile) + self.bridge.register("registerNewAccount", self.registerNewAccount) + self.bridge.register("connect", self.connect) + self.bridge.register("disconnect", self.disconnect) + self.bridge.register("getContacts", self.memory.getContacts) + self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus) + self.bridge.register("getWaitingSub", self.memory.getWaitingSub) + self.bridge.register("sendMessage", self.sendMessage) + self.bridge.register("setParam", self.setParam) + self.bridge.register("getParamA", self.memory.getParamA) + self.bridge.register("getParamsUI", self.memory.getParamsUI) + self.bridge.register("getParams", self.memory.getParams) + self.bridge.register("getParamsForCategory", self.memory.getParamsForCategory) + self.bridge.register("getParamsCategories", self.memory.getParamsCategories) + self.bridge.register("getHistory", self.memory.getHistory) + self.bridge.register("setPresence", self.setPresence) + self.bridge.register("subscription", self.subscription) + self.bridge.register("addContact", self.addContact) + self.bridge.register("delContact", self.delContact) + self.bridge.register("isConnected", self.isConnected) + self.bridge.register("launchAction", self.launchAction) + self.bridge.register("confirmationAnswer", self.confirmationAnswer) + self.bridge.register("getProgress", self.getProgress) + self.bridge.register("getMenus", self.getMenus) + self.bridge.register("getMenuHelp", self.getMenuHelp) + self.bridge.register("callMenu", self.callMenu) + + self._import_plugins() + + + def _import_plugins(self): + """Import all plugins found in plugins directory""" + import sat.plugins + plugins_path = os.path.dirname(sat.plugins.__file__) + plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob (os.path.join(plugins_path,"plugin*.py")))] + __plugins_to_import = {} #plugins will still have to import + for plug in plug_lst: + plugin_path = 'sat.plugins.'+plug + __import__(plugin_path) + mod = sys.modules[plugin_path] + plugin_info = mod.PLUGIN_INFO + __plugins_to_import[plugin_info['import_name']] = (plugin_path, mod, plugin_info) + while True: + self._import_plugins_from_dict(__plugins_to_import) + if not __plugins_to_import: + break + + def _import_plugins_from_dict(self, plugins_to_import, import_name=None): + """Recursively import and their dependencies in the right order + @param plugins_to_import: dict where key=import_name and values= (plugin_path, module, plugin_info)""" + if self.plugins.has_key(import_name): + debug('Plugin [%s] already imported, passing' % import_name) + return + if not import_name: + import_name,(plugin_path, mod, plugin_info) = plugins_to_import.popitem() + else: + if not import_name in plugins_to_import: + raise ImportError(_('Dependency plugin not found: [%s]') % import_name) + plugin_path, mod, plugin_info = plugins_to_import.pop(import_name) + dependencies = plugin_info.setdefault("dependencies",[]) + for dependency in dependencies: + if not self.plugins.has_key(dependency): + debug('Recursively import dependency of [%s]: [%s]' % (import_name, dependency)) + self._import_plugins_from_dict(plugins_to_import, dependency) + info (_("importing plugin: %s"), plugin_info['name']) + self.plugins[import_name] = getattr(mod, plugin_info['main'])(self) + if plugin_info.has_key('handler') and plugin_info['handler'] == 'yes': + self.plugins[import_name].is_handler = True + else: + self.plugins[import_name].is_handler = False + #TODO: test xmppclient presence and register handler parent + + def connect(self, profile_key = '@DEFAULT@'): + """Connect to jabber server""" + + profile = self.memory.getProfileName(profile_key) + if not profile: + error (_('Trying to connect a non-exsitant profile')) + return + + if (self.isConnected(profile)): + info(_("already connected !")) + return + current = self.profiles[profile] = SatXMPPClient(self, profile, + jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile), profile), + self.memory.getParamA("Password", "Connection", profile_key = profile), + self.memory.getParamA("Server", "Connection", profile_key = profile), 5222) + + current.messageProt = SatMessageProtocol(self) + current.messageProt.setHandlerParent(current) + + current.roster = SatRosterProtocol(self) + current.roster.setHandlerParent(current) + + current.presence = SatPresenceProtocol(self) + current.presence.setHandlerParent(current) + + current.fallBack = SatFallbackHandler(self) + current.fallBack.setHandlerParent(current) + + current.versionHandler = SatVersionHandler(self.get_const('client_name'), + self.get_const('client_version')) + current.versionHandler.setHandlerParent(current) + + debug (_("setting plugins parents")) + + for plugin in self.plugins.iteritems(): + if plugin[1].is_handler: + plugin[1].getHandler(profile).setHandlerParent(current) + + current.startService() + + def disconnect(self, profile_key='@DEFAULT@'): + """disconnect from jabber server""" + if (not self.isConnected(profile_key)): + info(_("not connected !")) + return + profile = self.memory.getProfileName(profile_key) + info(_("Disconnecting...")) + self.profiles[profile].stopService() + + def startService(self): + info("Salut à toi ô mon frère !") + #TODO: manage autoconnect + #self.connect() + + def stopService(self): + self.memory.save() + info("Salut aussi à Rantanplan") + + def run(self): + debug(_("running app")) + reactor.run() + + def stop(self): + debug(_("stopping app")) + reactor.stop() + + ## Misc methods ## + + def getJidNStream(self, profile_key): + """Convenient method to get jid and stream from profile key + @return: tuple (jid, xmlstream) from profile, can be None""" + profile = self.memory.getProfileName(profile_key) + if not profile or not self.profiles[profile].isConnected(): + return (None, None) + return (self.profiles[profile].jid, self.profiles[profile].xmlstream) + + def getClient(self, profile_key): + """Convenient method to get client from profile key + @return: client or None if it doesn't exist""" + profile = self.memory.getProfileName(profile_key) + if not profile: + return None + return self.profiles[profile] + + def registerNewAccount(self, login, password, server, port = 5222, id = None): + """Connect to a server and create a new account using in-band registration""" + + next_id = id or sat_next_id() #the id is used to send server's answer + serverRegistrer = xmlstream.XmlStreamFactory(RegisteringAuthenticator(self, server, login, password, next_id)) + connector = reactor.connectTCP(server, port, serverRegistrer) + serverRegistrer.clientConnectionLost = lambda conn, reason: connector.disconnect() + + return next_id + + def registerNewAccountCB(self, id, data, profile): + user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] + password = self.memory.getParamA("Password", "Connection", profile_key=profile) + server = self.memory.getParamA("Server", "Connection", profile_key=profile) + + if not user or not password or not server: + info (_('No user or server given')) + #TODO: a proper error message must be sent to frontend + self.actionResult(id, "ERROR", {'message':_("No user, password or server given, can't register new account.")}) + return + + confirm_id = sat_next_id() + self.__private_data[confirm_id]=(id,profile) + + self.askConfirmation(confirm_id, "YES/NO", + {"message":_("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user':user, 'server':server, 'profile':profile}}, + self.regisConfirmCB) + print ("===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============") + print "id=",id + print "data=",data + + def regisConfirmCB(self, id, accepted, data): + print _("register Confirmation CB ! (%s)") % str(accepted) + action_id,profile = self.__private_data[id] + del self.__private_data[id] + if accepted: + user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] + password = self.memory.getParamA("Password", "Connection", profile_key=profile) + server = self.memory.getParamA("Server", "Connection", profile_key=profile) + self.registerNewAccount(user, password, server, id=action_id) + else: + self.actionResult(action_id, "SUPPRESS", {}) + + def submitForm(self, action, target, fields, profile_key='@DEFAULT@'): + """submit a form + @param target: target jid where we are submitting + @param fields: list of tuples (name, value) + @return: tuple: (id, deferred) + """ + + profile = self.memory.getProfileName(profile_key) + assert(profile) + to_jid = jid.JID(target) + + iq = compat.IQ(self.profiles[profile].xmlstream, 'set') + iq["to"] = target + iq["from"] = self.profiles[profile].jid.full() + query = iq.addElement(('jabber:iq:register', 'query')) + if action=='SUBMIT': + form = tupleList2dataForm(fields) + query.addChild(form.toElement()) + elif action=='CANCEL': + query.addElement('remove') + else: + error (_("FIXME FIXME FIXME: Unmanaged action (%s) in submitForm") % action) + raise NotImplementedError + + deferred = iq.send(target) + return (iq['id'], deferred) + + ## Client management ## + + def setParam(self, name, value, category, profile_key='@DEFAULT@'): + """set wanted paramater and notice observers""" + info (_("setting param: %(name)s=%(value)s in category %(category)s") % {'name':name, 'value':value, 'category':category}) + self.memory.setParam(name, value, category, profile_key) + + def isConnected(self, profile_key='@DEFAULT@'): + """Return connection status of profile + @param profile_key: key_word or profile name to determine profile name + @return True if connected + """ + profile = self.memory.getProfileName(profile_key) + if not profile: + error (_('asking connection status for a non-existant profile')) + return + if not self.profiles.has_key(profile): + return False + return self.profiles[profile].isConnected() + + def launchAction(self, type, data, profile_key='@DEFAULT@'): + """Launch a specific action asked by client + @param type: action type (button) + @param data: needed data to launch the action + + @return: action id for result, or empty string in case or error + """ + profile = self.memory.getProfileName(profile_key) + if not profile: + error (_('trying to launch action with a non-existant profile')) + raise Exception #TODO: raise a proper exception + if type=="button": + try: + cb_name = data['callback_id'] + except KeyError: + error (_("Incomplete data")) + return "" + id = sat_next_id() + self.callGeneralCB(cb_name, id, data, profile = profile) + return id + else: + error (_("Unknown action type")) + return "" + + + ## jabber methods ## + + def sendMessage(self, to, msg, subject=None, type='chat', profile_key='@DEFAULT@'): + #FIXME: check validity of recipient + profile = self.memory.getProfileName(profile_key) + assert(profile) + current_jid = self.profiles[profile].jid + debug(_("Sending jabber message to %s..."), to) + message = domish.Element(('jabber:client','message')) + message["to"] = jid.JID(to).full() + message["from"] = current_jid.full() + message["type"] = type + if subject: + message.addElement("subject", "jabber:client", subject) + message.addElement("body", "jabber:client", msg) + self.profiles[profile].xmlstream.send(message) + self.memory.addToHistory(current_jid, current_jid, jid.JID(to), message["type"], unicode(msg)) + if type!="groupchat": + self.bridge.newMessage(message['from'], unicode(msg), mess_type=type, to_jid=message['to'], profile=profile) #We send back the message, so all clients are aware of it + + + def setPresence(self, to="", show="", priority = 0, statuses={}, profile_key='@DEFAULT@'): + """Send our presence information""" + profile = self.memory.getProfileName(profile_key) + assert(profile) + to_jid = jid.JID(to) if to else None + self.profiles[profile].presence.available(to_jid, show, statuses, priority) + #XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not broadcasted to generating resource) + if statuses.has_key(''): + statuses['default'] = statuses[''] + del statuses[''] + self.bridge.presenceUpdate(self.profiles[profile].jid.full(), show, + int(priority), statuses, profile) + + + def subscription(self, subs_type, raw_jid, profile_key='@DEFAULT@'): + """Called to manage subscription + @param subs_type: subsciption type (cf RFC 3921) + @param raw_jid: unicode entity's jid + @param profile_key: profile""" + profile = self.memory.getProfileName(profile_key) + assert(profile) + to_jid = jid.JID(raw_jid) + debug (_('subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type':subs_type, 'jid':to_jid.full()}) + if subs_type=="subscribe": + self.profiles[profile].presence.subscribe(to_jid) + elif subs_type=="subscribed": + self.profiles[profile].presence.subscribed(to_jid) + contact = self.memory.getContact(to_jid) + if not contact or not bool(contact['to']): #we automatically subscribe to 'to' presence + debug(_('sending automatic "to" subscription request')) + self.subscription('subscribe', to_jid.userhost()) + elif subs_type=="unsubscribe": + self.profiles[profile].presence.unsubscribe(to_jid) + elif subs_type=="unsubscribed": + self.profiles[profile].presence.unsubscribed(to_jid) + + + def addContact(self, to, profile_key='@DEFAULT@'): + """Add a contact in roster list""" + profile = self.memory.getProfileName(profile_key) + assert(profile) + to_jid=jid.JID(to) + #self.profiles[profile].roster.addItem(to_jid) XXX: disabled (cf http://wokkel.ik.nu/ticket/56)) + self.profiles[profile].presence.subscribe(to_jid) + + def delContact(self, to, profile_key='@DEFAULT@'): + """Remove contact from roster list""" + profile = self.memory.getProfileName(profile_key) + assert(profile) + to_jid=jid.JID(to) + self.profiles[profile].roster.removeItem(to_jid) + self.profiles[profile].presence.unsubscribe(to_jid) + self.bridge.contactDeleted(to, profile) + + + ## callbacks ## + + def serverDisco(self, disco, profile): + """xep-0030 Discovery Protocol.""" + for feature in disco.features: + debug (_("Feature found: %s"),feature) + self.memory.addServerFeature(feature, profile) + for cat, type in disco.identities: + debug (_("Identity found: [%(category)s/%(type)s] %(identity)s") % {'category':cat, 'type':type, 'identity':disco.identities[(cat,type)]}) + + def serverDiscoItems(self, disco_result, disco_client, profile, initialized): + """xep-0030 Discovery Protocol. + @param disco_result: result of the disco item querry + @param disco_client: SatDiscoProtocol instance + @param profile: profile of the user + @param initialized: deferred which must be chained when everything is done""" + def _check_entity_cb(result, entity, profile): + for category, type in result.identities: + debug (_('Identity added: (%(category)s,%(type)s) ==> %(entity)s [%(profile)s]') % { + 'category':category, 'type':type, 'entity':entity, 'profile':profile}) + self.memory.addServerIdentity(category, type, entity, profile) + + defer_list = [] + for item in disco_result._items: + defer_list.append(disco_client.requestInfo(item.entity).addCallback(_check_entity_cb, item.entity, profile)) + defer.DeferredList(defer_list).chainDeferred(initialized) + + + ## Generic HMI ## + + def actionResult(self, id, type, data): + """Send the result of an action + @param id: same id used with action + @param type: result type ("PARAM", "SUCCESS", "ERROR", "XMLUI") + @param data: dictionary + """ + self.bridge.actionResult(type, id, data) + + def actionResultExt(self, id, type, data): + """Send the result of an action, extended version + @param id: same id used with action + @param type: result type /!\ only "DICT_DICT" for this method + @param data: dictionary of dictionaries + """ + if type != "DICT_DICT": + error(_("type for actionResultExt must be DICT_DICT, fixing it")) + type = "DICT_DICT" + self.bridge.actionResultExt(type, id, data) + + + + def askConfirmation(self, id, type, data, cb): + """Add a confirmation callback + @param id: id used to get answer + @param type: confirmation type ("YES/NO", "FILE_TRANSFERT") + @param data: data (depend of confirmation type) + @param cb: callback called with the answer + """ + if self.__waiting_conf.has_key(id): + error (_("Attempt to register two callbacks for the same confirmation")) + else: + self.__waiting_conf[id] = cb + self.bridge.askConfirmation(type, id, data) + + + def confirmationAnswer(self, id, accepted, data): + """Called by frontends to answer confirmation requests""" + debug (_("Received confirmation answer for id [%(id)s]: %(success)s") % {'id': id, 'success':_("accepted") if accepted else _("refused")}) + if not self.__waiting_conf.has_key(id): + error (_("Received an unknown confirmation")) + else: + cb = self.__waiting_conf[id] + del self.__waiting_conf[id] + cb(id, accepted, data) + + def registerProgressCB(self, id, CB): + """Register a callback called when progress is requested for id""" + self.__progress_cb_map[id] = CB + + def removeProgressCB(self, id): + """Remove a progress callback""" + if not self.__progress_cb_map.has_key(id): + error (_("Trying to remove an unknow progress callback")) + else: + del self.__progress_cb_map[id] + + def getProgress(self, id): + """Return a dict with progress information + data['position'] : current possition + data['size'] : end_position + """ + data = {} + try: + self.__progress_cb_map[id](data) + except KeyError: + pass + #debug("Requested progress for unknown id") + return data + + def registerGeneralCB(self, name, CB): + """Register a callback called for general reason""" + self.__general_cb_map[name] = CB + + def removeGeneralCB(self, name): + """Remove a general callback""" + if not self.__general_cb_map.has_key(name): + error (_("Trying to remove an unknow general callback")) + else: + del self.__general_cb_map[name] + + def callGeneralCB(self, name, *args, **kwargs): + """Call general function back""" + try: + return self.__general_cb_map[name](*args, **kwargs) + except KeyError: + error(_("Trying to call unknown function (%s)") % name) + return None + + #Menus management + + def importMenu(self, category, name, callback, help_string = "", type = "NORMAL"): + """register a new menu for frontends + @param category: category of the menu + @param name: menu item entry + @param callback: method to be called when menuitem is selected""" + if self.menus.has_key((category,name)): + error ("Want to register a menu which already existe") + return + self.menus[(category,name,type)] = {'callback':callback, 'help_string':help_string, 'type':type} + + def getMenus(self): + """Return all menus registered""" + return self.menus.keys() + + def getMenuHelp(self, category, name, type="NORMAL"): + """return the help string of the menu""" + try: + return self.menus[(category,name,type)]['help_string'] + except KeyError: + error (_("Trying to access an unknown menu")) + return "" + + def callMenu(self, category, name, type="NORMAL", profile_key='@DEFAULT@'): + """return the id of the action""" + profile = self.memory.getProfileName(profile_key) + if not profile_key: + error (_('Non-exsitant profile')) + return "" + if self.menus.has_key((category,name,type)): + id = self.get_next_id() + self.menus[(category,name,type)]['callback'](id, profile) + return id + else: + error (_("Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)")%{'category':category, 'name':name,'type':type}) + return "" diff -r 608a4a2ba94e -r 0a8eb0461f31 src/sat.tac --- a/src/sat.tac Mon May 23 21:18:58 2011 +0200 +++ b/src/sat.tac Mon May 23 21:32:28 2011 +0200 @@ -19,606 +19,14 @@ along with this program. If not, see . """ -CONST = { - 'client_name' : u'SàT (Salut à toi)', - 'client_version' : u'0.1.1D', #Please add 'D' at the end for dev versions - 'local_dir' : '~/.sat' -} - import gettext gettext.install('sat', "i18n", unicode=True) -from twisted.application import internet, service -from twisted.internet import glib2reactor, protocol, task, defer +from twisted.application import service +from twisted.internet import glib2reactor glib2reactor.install() -from twisted.words.protocols.jabber import jid, xmlstream -from twisted.words.protocols.jabber import error as jab_error -from twisted.words.xish import domish - -from twisted.internet import reactor -import pdb - -from wokkel import client, disco, xmppim, generic, compat - -from sat.bridge.DBus import DBusBridge -import logging -from logging import debug, info, error - -import signal, sys -import os.path - -from sat.core.xmpp import SatXMPPClient, SatMessageProtocol, SatRosterProtocol, SatPresenceProtocol, SatDiscoProtocol, SatFallbackHandler, RegisteringAuthenticator, SatVersionHandler -from sat.tools.memory import Memory -from sat.tools.xml_tools import tupleList2dataForm -from sat.tools.misc import TriggerManager -from glob import glob - -try: - from twisted.words.protocols.xmlstream import XMPPHandler -except ImportError: - from wokkel.subprotocols import XMPPHandler - - -### logging configuration FIXME: put this elsewhere ### -logging.basicConfig(level=logging.DEBUG, - format='%(message)s') -### - - -sat_id = 0 - -def sat_next_id(): - global sat_id - sat_id+=1 - return "sat_id_"+str(sat_id) - -class SAT(service.Service): - - def get_next_id(self): - return sat_next_id() - - def get_const(self, name): - """Return a constant""" - if not CONST.has_key(name): - error(_('Trying to access an undefined constant')) - raise Exception - return CONST[name] - - def set_const(self, name, value): - """Save a constant""" - if CONST.has_key(name): - error(_('Trying to redefine a constant')) - raise Exception - CONST[name] = value - - def __init__(self): - #TODO: standardize callback system - - local_dir = os.path.expanduser(self.get_const('local_dir')) - if not os.path.exists(local_dir): - os.makedirs(local_dir) - - self.__waiting_conf = {} #callback called when a confirmation is received - self.__progress_cb_map = {} #callback called when a progress is requested (key = progress id) - self.__general_cb_map = {} #callback called for general reasons (key = name) - self.__private_data = {} #used for internal callbacks (key = id) - self.trigger = TriggerManager() #trigger are user to change SàT behaviour - self.profiles = {} - self.plugins = {} - self.menus = {} #used to know which new menus are wanted by plugins - - self.memory=Memory(self) - - self.bridge=DBusBridge() - self.bridge.register("getVersion", lambda: self.get_const('client_version')) - self.bridge.register("getProfileName", self.memory.getProfileName) - self.bridge.register("getProfilesList", self.memory.getProfilesList) - self.bridge.register("createProfile", self.memory.createProfile) - self.bridge.register("deleteProfile", self.memory.deleteProfile) - self.bridge.register("registerNewAccount", self.registerNewAccount) - self.bridge.register("connect", self.connect) - self.bridge.register("disconnect", self.disconnect) - self.bridge.register("getContacts", self.memory.getContacts) - self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus) - self.bridge.register("getWaitingSub", self.memory.getWaitingSub) - self.bridge.register("sendMessage", self.sendMessage) - self.bridge.register("setParam", self.setParam) - self.bridge.register("getParamA", self.memory.getParamA) - self.bridge.register("getParamsUI", self.memory.getParamsUI) - self.bridge.register("getParams", self.memory.getParams) - self.bridge.register("getParamsForCategory", self.memory.getParamsForCategory) - self.bridge.register("getParamsCategories", self.memory.getParamsCategories) - self.bridge.register("getHistory", self.memory.getHistory) - self.bridge.register("setPresence", self.setPresence) - self.bridge.register("subscription", self.subscription) - self.bridge.register("addContact", self.addContact) - self.bridge.register("delContact", self.delContact) - self.bridge.register("isConnected", self.isConnected) - self.bridge.register("launchAction", self.launchAction) - self.bridge.register("confirmationAnswer", self.confirmationAnswer) - self.bridge.register("getProgress", self.getProgress) - self.bridge.register("getMenus", self.getMenus) - self.bridge.register("getMenuHelp", self.getMenuHelp) - self.bridge.register("callMenu", self.callMenu) - - self._import_plugins() - - - def _import_plugins(self): - """Import all plugins found in plugins directory""" - import sat.plugins - plugins_path = os.path.dirname(sat.plugins.__file__) - plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob (os.path.join(plugins_path,"plugin*.py")))] - __plugins_to_import = {} #plugins will still have to import - for plug in plug_lst: - plugin_path = 'sat.plugins.'+plug - __import__(plugin_path) - mod = sys.modules[plugin_path] - plugin_info = mod.PLUGIN_INFO - __plugins_to_import[plugin_info['import_name']] = (plugin_path, mod, plugin_info) - while True: - self._import_plugins_from_dict(__plugins_to_import) - if not __plugins_to_import: - break - - def _import_plugins_from_dict(self, plugins_to_import, import_name=None): - """Recursively import and their dependencies in the right order - @param plugins_to_import: dict where key=import_name and values= (plugin_path, module, plugin_info)""" - if self.plugins.has_key(import_name): - debug('Plugin [%s] already imported, passing' % import_name) - return - if not import_name: - import_name,(plugin_path, mod, plugin_info) = plugins_to_import.popitem() - else: - if not import_name in plugins_to_import: - raise ImportError(_('Dependency plugin not found: [%s]') % import_name) - plugin_path, mod, plugin_info = plugins_to_import.pop(import_name) - dependencies = plugin_info.setdefault("dependencies",[]) - for dependency in dependencies: - if not self.plugins.has_key(dependency): - debug('Recursively import dependency of [%s]: [%s]' % (import_name, dependency)) - self._import_plugins_from_dict(plugins_to_import, dependency) - info (_("importing plugin: %s"), plugin_info['name']) - self.plugins[import_name] = getattr(mod, plugin_info['main'])(self) - if plugin_info.has_key('handler') and plugin_info['handler'] == 'yes': - self.plugins[import_name].is_handler = True - else: - self.plugins[import_name].is_handler = False - #TODO: test xmppclient presence and register handler parent - - def connect(self, profile_key = '@DEFAULT@'): - """Connect to jabber server""" - - profile = self.memory.getProfileName(profile_key) - if not profile: - error (_('Trying to connect a non-exsitant profile')) - return - - if (self.isConnected(profile)): - info(_("already connected !")) - return - current = self.profiles[profile] = SatXMPPClient(self, profile, - jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile), profile), - self.memory.getParamA("Password", "Connection", profile_key = profile), - self.memory.getParamA("Server", "Connection", profile_key = profile), 5222) - - current.messageProt = SatMessageProtocol(self) - current.messageProt.setHandlerParent(current) - - current.roster = SatRosterProtocol(self) - current.roster.setHandlerParent(current) - - current.presence = SatPresenceProtocol(self) - current.presence.setHandlerParent(current) - - current.fallBack = SatFallbackHandler(self) - current.fallBack.setHandlerParent(current) - - current.versionHandler = SatVersionHandler(self.get_const('client_name'), - self.get_const('client_version')) - current.versionHandler.setHandlerParent(current) - - debug (_("setting plugins parents")) - - for plugin in self.plugins.iteritems(): - if plugin[1].is_handler: - plugin[1].getHandler(profile).setHandlerParent(current) - - current.startService() - - def disconnect(self, profile_key='@DEFAULT@'): - """disconnect from jabber server""" - if (not self.isConnected(profile_key)): - info(_("not connected !")) - return - profile = self.memory.getProfileName(profile_key) - info(_("Disconnecting...")) - self.profiles[profile].stopService() - - def startService(self): - info("Salut à toi ô mon frère !") - #TODO: manage autoconnect - #self.connect() - - def stopService(self): - self.memory.save() - info("Salut aussi à Rantanplan") - - def run(self): - debug(_("running app")) - reactor.run() - - def stop(self): - debug(_("stopping app")) - reactor.stop() - - ## Misc methods ## - - def getJidNStream(self, profile_key): - """Convenient method to get jid and stream from profile key - @return: tuple (jid, xmlstream) from profile, can be None""" - profile = self.memory.getProfileName(profile_key) - if not profile or not self.profiles[profile].isConnected(): - return (None, None) - return (self.profiles[profile].jid, self.profiles[profile].xmlstream) - - def getClient(self, profile_key): - """Convenient method to get client from profile key - @return: client or None if it doesn't exist""" - profile = self.memory.getProfileName(profile_key) - if not profile: - return None - return self.profiles[profile] - - def registerNewAccount(self, login, password, server, port = 5222, id = None): - """Connect to a server and create a new account using in-band registration""" - - next_id = id or sat_next_id() #the id is used to send server's answer - serverRegistrer = xmlstream.XmlStreamFactory(RegisteringAuthenticator(self, server, login, password, next_id)) - connector = reactor.connectTCP(server, port, serverRegistrer) - serverRegistrer.clientConnectionLost = lambda conn, reason: connector.disconnect() - - return next_id - - def registerNewAccountCB(self, id, data, profile): - user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] - password = self.memory.getParamA("Password", "Connection", profile_key=profile) - server = self.memory.getParamA("Server", "Connection", profile_key=profile) - - if not user or not password or not server: - info (_('No user or server given')) - #TODO: a proper error message must be sent to frontend - self.actionResult(id, "ERROR", {'message':_("No user, password or server given, can't register new account.")}) - return - - confirm_id = sat_next_id() - self.__private_data[confirm_id]=(id,profile) - - self.askConfirmation(confirm_id, "YES/NO", - {"message":_("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user':user, 'server':server, 'profile':profile}}, - self.regisConfirmCB) - print ("===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============") - print "id=",id - print "data=",data - - def regisConfirmCB(self, id, accepted, data): - print _("register Confirmation CB ! (%s)") % str(accepted) - action_id,profile = self.__private_data[id] - del self.__private_data[id] - if accepted: - user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] - password = self.memory.getParamA("Password", "Connection", profile_key=profile) - server = self.memory.getParamA("Server", "Connection", profile_key=profile) - self.registerNewAccount(user, password, server, id=action_id) - else: - self.actionResult(action_id, "SUPPRESS", {}) - - def submitForm(self, action, target, fields, profile_key='@DEFAULT@'): - """submit a form - @param target: target jid where we are submitting - @param fields: list of tuples (name, value) - @return: tuple: (id, deferred) - """ - - profile = self.memory.getProfileName(profile_key) - assert(profile) - to_jid = jid.JID(target) - - iq = compat.IQ(self.profiles[profile].xmlstream, 'set') - iq["to"] = target - iq["from"] = self.profiles[profile].jid.full() - query = iq.addElement(('jabber:iq:register', 'query')) - if action=='SUBMIT': - form = tupleList2dataForm(fields) - query.addChild(form.toElement()) - elif action=='CANCEL': - query.addElement('remove') - else: - error (_("FIXME FIXME FIXME: Unmanaged action (%s) in submitForm") % action) - raise NotImplementedError - - deferred = iq.send(target) - return (iq['id'], deferred) - - ## Client management ## - - def setParam(self, name, value, category, profile_key='@DEFAULT@'): - """set wanted paramater and notice observers""" - info (_("setting param: %(name)s=%(value)s in category %(category)s") % {'name':name, 'value':value, 'category':category}) - self.memory.setParam(name, value, category, profile_key) - - def isConnected(self, profile_key='@DEFAULT@'): - """Return connection status of profile - @param profile_key: key_word or profile name to determine profile name - @return True if connected - """ - profile = self.memory.getProfileName(profile_key) - if not profile: - error (_('asking connection status for a non-existant profile')) - return - if not self.profiles.has_key(profile): - return False - return self.profiles[profile].isConnected() - - def launchAction(self, type, data, profile_key='@DEFAULT@'): - """Launch a specific action asked by client - @param type: action type (button) - @param data: needed data to launch the action - - @return: action id for result, or empty string in case or error - """ - profile = self.memory.getProfileName(profile_key) - if not profile: - error (_('trying to launch action with a non-existant profile')) - raise Exception #TODO: raise a proper exception - if type=="button": - try: - cb_name = data['callback_id'] - except KeyError: - error (_("Incomplete data")) - return "" - id = sat_next_id() - self.callGeneralCB(cb_name, id, data, profile = profile) - return id - else: - error (_("Unknown action type")) - return "" - - - ## jabber methods ## - - def sendMessage(self, to, msg, subject=None, type='chat', profile_key='@DEFAULT@'): - #FIXME: check validity of recipient - profile = self.memory.getProfileName(profile_key) - assert(profile) - current_jid = self.profiles[profile].jid - debug(_("Sending jabber message to %s..."), to) - message = domish.Element(('jabber:client','message')) - message["to"] = jid.JID(to).full() - message["from"] = current_jid.full() - message["type"] = type - if subject: - message.addElement("subject", "jabber:client", subject) - message.addElement("body", "jabber:client", msg) - self.profiles[profile].xmlstream.send(message) - self.memory.addToHistory(current_jid, current_jid, jid.JID(to), message["type"], unicode(msg)) - if type!="groupchat": - self.bridge.newMessage(message['from'], unicode(msg), mess_type=type, to_jid=message['to'], profile=profile) #We send back the message, so all clients are aware of it - - - def setPresence(self, to="", show="", priority = 0, statuses={}, profile_key='@DEFAULT@'): - """Send our presence information""" - profile = self.memory.getProfileName(profile_key) - assert(profile) - to_jid = jid.JID(to) if to else None - self.profiles[profile].presence.available(to_jid, show, statuses, priority) - #XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not broadcasted to generating resource) - if statuses.has_key(''): - statuses['default'] = statuses[''] - del statuses[''] - self.bridge.presenceUpdate(self.profiles[profile].jid.full(), show, - int(priority), statuses, profile) - - - def subscription(self, subs_type, raw_jid, profile_key='@DEFAULT@'): - """Called to manage subscription - @param subs_type: subsciption type (cf RFC 3921) - @param raw_jid: unicode entity's jid - @param profile_key: profile""" - profile = self.memory.getProfileName(profile_key) - assert(profile) - to_jid = jid.JID(raw_jid) - debug (_('subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type':subs_type, 'jid':to_jid.full()}) - if subs_type=="subscribe": - self.profiles[profile].presence.subscribe(to_jid) - elif subs_type=="subscribed": - self.profiles[profile].presence.subscribed(to_jid) - contact = self.memory.getContact(to_jid) - if not contact or not bool(contact['to']): #we automatically subscribe to 'to' presence - debug(_('sending automatic "to" subscription request')) - self.subscription('subscribe', to_jid.userhost()) - elif subs_type=="unsubscribe": - self.profiles[profile].presence.unsubscribe(to_jid) - elif subs_type=="unsubscribed": - self.profiles[profile].presence.unsubscribed(to_jid) - - - def addContact(self, to, profile_key='@DEFAULT@'): - """Add a contact in roster list""" - profile = self.memory.getProfileName(profile_key) - assert(profile) - to_jid=jid.JID(to) - #self.profiles[profile].roster.addItem(to_jid) XXX: disabled (cf http://wokkel.ik.nu/ticket/56)) - self.profiles[profile].presence.subscribe(to_jid) - - def delContact(self, to, profile_key='@DEFAULT@'): - """Remove contact from roster list""" - profile = self.memory.getProfileName(profile_key) - assert(profile) - to_jid=jid.JID(to) - self.profiles[profile].roster.removeItem(to_jid) - self.profiles[profile].presence.unsubscribe(to_jid) - self.bridge.contactDeleted(to, profile) - - - ## callbacks ## - - def serverDisco(self, disco, profile): - """xep-0030 Discovery Protocol.""" - for feature in disco.features: - debug (_("Feature found: %s"),feature) - self.memory.addServerFeature(feature, profile) - for cat, type in disco.identities: - debug (_("Identity found: [%(category)s/%(type)s] %(identity)s") % {'category':cat, 'type':type, 'identity':disco.identities[(cat,type)]}) - - def serverDiscoItems(self, disco_result, disco_client, profile, initialized): - """xep-0030 Discovery Protocol. - @param disco_result: result of the disco item querry - @param disco_client: SatDiscoProtocol instance - @param profile: profile of the user - @param initialized: deferred which must be chained when everything is done""" - def _check_entity_cb(result, entity, profile): - for category, type in result.identities: - debug (_('Identity added: (%(category)s,%(type)s) ==> %(entity)s [%(profile)s]') % { - 'category':category, 'type':type, 'entity':entity, 'profile':profile}) - self.memory.addServerIdentity(category, type, entity, profile) - - defer_list = [] - for item in disco_result._items: - defer_list.append(disco_client.requestInfo(item.entity).addCallback(_check_entity_cb, item.entity, profile)) - defer.DeferredList(defer_list).chainDeferred(initialized) - - - ## Generic HMI ## - - def actionResult(self, id, type, data): - """Send the result of an action - @param id: same id used with action - @param type: result type ("PARAM", "SUCCESS", "ERROR", "XMLUI") - @param data: dictionary - """ - self.bridge.actionResult(type, id, data) - - def actionResultExt(self, id, type, data): - """Send the result of an action, extended version - @param id: same id used with action - @param type: result type /!\ only "DICT_DICT" for this method - @param data: dictionary of dictionaries - """ - if type != "DICT_DICT": - error(_("type for actionResultExt must be DICT_DICT, fixing it")) - type = "DICT_DICT" - self.bridge.actionResultExt(type, id, data) - - - - def askConfirmation(self, id, type, data, cb): - """Add a confirmation callback - @param id: id used to get answer - @param type: confirmation type ("YES/NO", "FILE_TRANSFERT") - @param data: data (depend of confirmation type) - @param cb: callback called with the answer - """ - if self.__waiting_conf.has_key(id): - error (_("Attempt to register two callbacks for the same confirmation")) - else: - self.__waiting_conf[id] = cb - self.bridge.askConfirmation(type, id, data) - - - def confirmationAnswer(self, id, accepted, data): - """Called by frontends to answer confirmation requests""" - debug (_("Received confirmation answer for id [%(id)s]: %(success)s") % {'id': id, 'success':_("accepted") if accepted else _("refused")}) - if not self.__waiting_conf.has_key(id): - error (_("Received an unknown confirmation")) - else: - cb = self.__waiting_conf[id] - del self.__waiting_conf[id] - cb(id, accepted, data) - - def registerProgressCB(self, id, CB): - """Register a callback called when progress is requested for id""" - self.__progress_cb_map[id] = CB - - def removeProgressCB(self, id): - """Remove a progress callback""" - if not self.__progress_cb_map.has_key(id): - error (_("Trying to remove an unknow progress callback")) - else: - del self.__progress_cb_map[id] - - def getProgress(self, id): - """Return a dict with progress information - data['position'] : current possition - data['size'] : end_position - """ - data = {} - try: - self.__progress_cb_map[id](data) - except KeyError: - pass - #debug("Requested progress for unknown id") - return data - - def registerGeneralCB(self, name, CB): - """Register a callback called for general reason""" - self.__general_cb_map[name] = CB - - def removeGeneralCB(self, name): - """Remove a general callback""" - if not self.__general_cb_map.has_key(name): - error (_("Trying to remove an unknow general callback")) - else: - del self.__general_cb_map[name] - - def callGeneralCB(self, name, *args, **kwargs): - """Call general function back""" - try: - return self.__general_cb_map[name](*args, **kwargs) - except KeyError: - error(_("Trying to call unknown function (%s)") % name) - return None - - #Menus management - - def importMenu(self, category, name, callback, help_string = "", type = "NORMAL"): - """register a new menu for frontends - @param category: category of the menu - @param name: menu item entry - @param callback: method to be called when menuitem is selected""" - if self.menus.has_key((category,name)): - error ("Want to register a menu which already existe") - return - self.menus[(category,name,type)] = {'callback':callback, 'help_string':help_string, 'type':type} - - def getMenus(self): - """Return all menus registered""" - return self.menus.keys() - - def getMenuHelp(self, category, name, type="NORMAL"): - """return the help string of the menu""" - try: - return self.menus[(category,name,type)]['help_string'] - except KeyError: - error (_("Trying to access an unknown menu")) - return "" - - def callMenu(self, category, name, type="NORMAL", profile_key='@DEFAULT@'): - """return the id of the action""" - profile = self.memory.getProfileName(profile_key) - if not profile_key: - error (_('Non-exsitant profile')) - return "" - if self.menus.has_key((category,name,type)): - id = self.get_next_id() - self.menus[(category,name,type)]['callback'](id, profile) - return id - else: - error (_("Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)")%{'category':category, 'name':name,'type':type}) - return "" - - +from sat.core.sat_main import SAT application = service.Application('SàT') service = SAT()