Mercurial > libervia-backend
diff src/sat.tac @ 223:86d249b6d9b7
Files reorganisation
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 29 Dec 2010 01:06:29 +0100 |
parents | sat.tac@5c420b1f1df4 |
children | fd9b7834d98a |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/sat.tac Wed Dec 29 01:06:29 2010 +0100 @@ -0,0 +1,796 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +SAT: a jabber client +Copyright (C) 2009, 2010 Jérôme Poisson (goffi@goffi.org) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +CONST = { + 'client_name' : u'SàT (Salut à toi)', + 'client_version' : u'0.0.3D', #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 +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.tools.memory import Memory +from sat.tools.xml_tools import tupleList2dataForm +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 SatXMPPClient(client.XMPPClient): + + def __init__(self, host_app, profile, user_jid, password, host=None, port=5222): + client.XMPPClient.__init__(self, user_jid, password, host, port) + self.factory.clientConnectionLost = self.connectionLost + self.__connected=False + self.profile = profile + self.host_app = host_app + + def _authd(self, xmlstream): + print "SatXMPPClient" + client.XMPPClient._authd(self, xmlstream) + self.__connected=True + info (_("********** [%s] CONNECTED **********") % self.profile) + self.streamInitialized() + self.host_app.bridge.connected(self.profile) #we send the signal to the clients + + def streamInitialized(self): + """Called after _authd""" + debug (_("XML stream is initialized")) + self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") #Needed to avoid disconnection (specially with openfire) + self.keep_alife.start(180) + + self.disco = SatDiscoProtocol(self) + self.disco.setHandlerParent(self) + self.discoHandler = disco.DiscoHandler() + self.discoHandler.setHandlerParent(self) + + self.roster.requestRoster() + + self.presence.available() + + self.disco.requestInfo(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDisco) #FIXME: use these informations + + def isConnected(self): + return self.__connected + + def connectionLost(self, connector, unused_reason): + self.__connected=False + info (_("********** [%s] DISCONNECTED **********") % self.profile) + try: + self.keep_alife.stop() + except AttributeError: + debug (_("No keep_alife")) + self.host_app.bridge.disconnected(self.profile) #we send the signal to the clients + + +class SatMessageProtocol(xmppim.MessageProtocol): + + def __init__(self, host): + xmppim.MessageProtocol.__init__(self) + self.host = host + + def onMessage(self, message): + debug (_(u"got message from: %s"), message["from"]) + for e in message.elements(): + if e.name == "body": + type = message['type'] if message.hasAttribute('type') else 'chat' #FIXME: check specs + self.host.bridge.newMessage(message["from"], e.children[0], type, profile=self.parent.profile) + self.host.memory.addToHistory(self.parent.jid, jid.JID(message["from"]), self.parent.jid, "chat", e.children[0]) + break + +class SatRosterProtocol(xmppim.RosterClientProtocol): + + def __init__(self, host): + xmppim.RosterClientProtocol.__init__(self) + self.host = host + + def rosterCb(self, roster): + for raw_jid, item in roster.iteritems(): + self.onRosterSet(item) + + def requestRoster(self): + """ ask the server for Roster list """ + debug("requestRoster") + self.getRoster().addCallback(self.rosterCb) + + def removeItem(self, to): + """Remove a contact from roster list""" + xmppim.RosterClientProtocol.removeItem(self, to) + #TODO: check IQ result + + #XXX: disabled (cf http://wokkel.ik.nu/ticket/56)) + #def addItem(self, to): + #"""Add a contact to roster list""" + #xmppim.RosterClientProtocol.addItem(self, to) + #TODO: check IQ result""" + + def onRosterSet(self, item): + """Called when a new/update roster item is received""" + #TODO: send a signal to frontends + item_attr = {'to': str(item.subscriptionTo), + 'from': str(item.subscriptionFrom), + 'ask': str(item.ask) + } + if item.name: + item_attr['name'] = item.name + info (_("new contact in roster list: %s"), item.jid.full()) + self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile) + self.host.bridge.newContact(item.jid.full(), item_attr, item.groups, self.parent.profile) + + def onRosterRemove(self, entity): + """Called when a roster removal event is received""" + #TODO: send a signal to frontends + print _("removing %s from roster list") % entity.full() + self.host.memory.delContact(entity, self.parent.profile) + +class SatPresenceProtocol(xmppim.PresenceClientProtocol): + + def __init__(self, host): + xmppim.PresenceClientProtocol.__init__(self) + self.host = host + + def availableReceived(self, entity, show=None, statuses=None, priority=0): + debug (_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity':entity, 'show':show, 'statuses':statuses, 'priority':priority}) + + if statuses.has_key(None): #we only want string keys + statuses["default"] = statuses[None] + del statuses[None] + + self.host.memory.addPresenceStatus(entity, show or "", + int(priority), statuses, self.parent.profile) + + #now it's time to notify frontends + self.host.bridge.presenceUpdate(entity.full(), show or "", + int(priority), statuses, self.parent.profile) + + def unavailableReceived(self, entity, statuses=None): + debug (_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity':entity, 'statuses':statuses}) + if statuses and statuses.has_key(None): #we only want string keys + statuses["default"] = statuses[None] + del statuses[None] + self.host.memory.addPresenceStatus(entity, "unavailable", 0, statuses, self.parent.profile) + + #now it's time to notify frontends + self.host.bridge.presenceUpdate(entity.full(), "unavailable", 0, statuses, self.parent.profile) + + + def available(self, entity=None, show=None, statuses=None, priority=0): + if statuses and statuses.has_key('default'): + statuses[None] = statuses['default'] + del statuses['default'] + xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority) + + def subscribedReceived(self, entity): + debug (_("subscription approved for [%s]") % entity.userhost()) + self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) + self.host.bridge.subscribe('subscribed', entity.userhost(), self.parent.profile) + + def unsubscribedReceived(self, entity): + debug (_("unsubscription confirmed for [%s]") % entity.userhost()) + self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) + self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile) + + def subscribeReceived(self, entity): + debug (_("subscription request for [%s]") % entity.userhost()) + self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile) + self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile) + + def unsubscribeReceived(self, entity): + debug (_("unsubscription asked for [%s]") % entity.userhost()) + self.host.memory.addWaitingSub('unsubscribe', entity.userhost(), self.parent.profile) + self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile) + +class SatDiscoProtocol(disco.DiscoClientProtocol): + def __init__(self, host): + disco.DiscoClientProtocol.__init__(self) + +class SatFallbackHandler(generic.FallbackHandler): + def __init__(self, host): + generic.FallbackHandler.__init__(self) + + def iqFallback(self, iq): + debug (u"iqFallback: xml = [%s], handled=%s" % (iq.toXml(), "True" if iq.handled else "False")) + generic.FallbackHandler.iqFallback(self, iq) + +class RegisteringAuthenticator(xmlstream.ConnectAuthenticator): + + def __init__(self, host, jabber_host, user_login, user_pass, answer_id): + xmlstream.ConnectAuthenticator.__init__(self, jabber_host) + self.host = host + self.jabber_host = jabber_host + self.user_login = user_login + self.user_pass = user_pass + self.answer_id = answer_id + print _("Registration asked for"),user_login, user_pass, jabber_host + + def connectionMade(self): + print "connectionMade" + + self.xmlstream.namespace = "jabber:client" + self.xmlstream.sendHeader() + + iq = compat.IQ(self.xmlstream, 'set') + iq["to"] = self.jabber_host + query = iq.addElement(('jabber:iq:register', 'query')) + _user = query.addElement('username') + _user.addContent(self.user_login) + _pass = query.addElement('password') + _pass.addContent(self.user_pass) + reg = iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure) + + def registrationAnswer(self, answer): + debug (_("registration answer: %s") % answer.toXml()) + answer_type = "SUCCESS" + answer_data={"message":_("Registration successfull")} + self.host.bridge.actionResult(answer_type, self.answer_id, answer_data) + self.xmlstream.sendFooter() + + def registrationFailure(self, failure): + info (_("Registration failure: %s") % str(failure.value)) + answer_type = "ERROR" + answer_data = {} + if failure.value.condition == 'conflict': + answer_data['reason'] = 'conflict' + answer_data={"message":_("Username already exists, please choose an other one")} + else: + answer_data['reason'] = 'unknown' + answer_data={"message":_("Registration failed (%s)") % str(failure.value.condition)} + self.host.bridge.actionResult(answer_type, self.answer_id, answer_data) + self.xmlstream.sendFooter() + + +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.profiles = {} + self.plugins = {} + self.menus = {} #used to know which new menus are wanted by plugins + + self.memory=Memory(self) + self.server_features=[] #XXX: temp dic, need to be transfered into self.memory in the future + + 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""" + #TODO: manage dependencies + plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob ("plugins/plugin*.py"))] + + for plug in plug_lst: + plug_path = 'plugins.'+plug + __import__(plug_path) + mod = sys.modules[plug_path] + plug_info = mod.PLUGIN_INFO + info (_("importing plugin: %s"), plug_info['name']) + self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self) + if plug_info.has_key('handler') and plug_info['handler'] == 'yes': + self.plugins[plug_info['import_name']].is_handler = True + else: + self.plugins[plug_info['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_key: + 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_key), profile), + self.memory.getParamA("Password", "Connection", profile_key = profile_key), + self.memory.getParamA("Server", "Connection", profile_key = profile_key), 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 = generic.VersionHandler(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,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 + 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), to=message['to'], type=type, 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) + + 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): + """xep-0030 Discovery Protocol.""" + for feature in disco.features: + debug (_("Feature found: %s"),feature) + self.server_features.append(feature) + for cat, type in disco.identities: + debug (_("Identity found: [%(category)s/%(type)s] %(identity)s") % {'category':cat, 'type':type, 'identity':disco.identities[(cat,type)]}) + + + ## 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 help string of the menu""" + 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 "" + + + +application = service.Application('SàT') +service = SAT() +service.setServiceParent(application)