changeset 331:0a8eb0461f31

core: main SAT class now moved in its own module core.sat_main
author Goffi <goffi@goffi.org>
date Mon, 23 May 2011 21:32:28 +0200
parents 608a4a2ba94e
children 8c9b9ef13ba1
files src/core/sat_main.py src/sat.tac
diffstat 2 files changed, 616 insertions(+), 595 deletions(-) [+]
line wrap: on
line diff
--- /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 <http://www.gnu.org/licenses/>.
+"""
+
+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 ""
--- 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 <http://www.gnu.org/licenses/>.
 """
 
-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()