diff --git a/src/core/sat_main.py b/src/core/sat_main.py
new file mode 100644
index 00000000..b21b76bf
--- /dev/null
+++ b/src/core/sat_main.py
@@ -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 --git a/src/sat.tac b/src/sat.tac
index a8fb6b89..c5589493 100644
--- a/src/sat.tac
+++ b/src/sat.tac
@@ -19,606 +19,14 @@ 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'
-}
-
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()