view src/core/ @ 427:bb0236633da5

core: outgoing groupchat message are not added to history anymore
author Goffi <>
date Fri, 11 Nov 2011 18:40:23 +0100
parents e4e9187e3b5b
children d7e5df876a04
line wrap: on
line source

# -*- coding: utf-8 -*-

SAT: a jabber client
Copyright (C) 2009, 2010, 2011  Jérôme Poisson (

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
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 <>.

    'client_name' : u'SàT (Salut à toi)',
    'client_version' : u'0.2.0D',   #Please add 'D' at the end for dev versions

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 wokkel.xmppim import RosterItem

from sat.bridge.DBus import DBusBridge
import logging
from logging import debug, info, error

import sys
import os.path

from sat.core import xmpp
from import Memory
from import tupleList2dataForm
from import TriggerManager
from glob import glob

    from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
    from wokkel.subprotocols import XMPPHandler

### logging configuration FIXME: put this elsewhere ###

sat_id = 0

def sat_next_id():
    global sat_id
    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"""
            _const = os.environ['SAT_CONST_%s' % name]
            if _const:
                debug(_("Constant %(name)s overrided with [%(value)s]") % {'name':name, 'value': _const})
                return _const
        except KeyError:
        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
        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
        local_dir = self.memory.getConfig('', 'local_dir')
        if not os.path.exists(local_dir):

        self.trigger = TriggerManager() #trigger are used to change SàT behaviour 
        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("asyncCreateProfile", self.memory.asyncCreateProfile)
        self.bridge.register("deleteProfile", self.memory.deleteProfile)
        self.bridge.register("registerNewAccount", self.registerNewAccount)
        self.bridge.register("connect", self.connect)
        self.bridge.register("asyncConnect", self.asyncConnect)
        self.bridge.register("disconnect", self.disconnect)
        self.bridge.register("getContacts", self.memory.getContacts)
        self.bridge.register("getLastResource", self.memory.getLastResource)
        self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus)
        self.bridge.register("getWaitingSub", self.memory.getWaitingSub)
        self.bridge.register("sendMessage", self.sendMessage)
        self.bridge.register("getConfig", self.memory.getConfig)
        self.bridge.register("setParam", self.setParam)
        self.bridge.register("getParamA", self.memory.getParamA)
        self.bridge.register("asyncGetParamA", self.memory.asyncGetParamA)
        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("updateContact", self.updateContact)
        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)

    def _postMemoryInit(self, ignore):
        """Method called after memory initialization is done"""
        info(_("Memory initialised"))

    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 we still have to import
        for plug in plug_lst:
            plugin_path = 'sat.plugins.'+plug
            mod = sys.modules[plugin_path]
            plugin_info = mod.PLUGIN_INFO
            __plugins_to_import[plugin_info['import_name']] = (plugin_path, mod, plugin_info)
        while True:
            if not __plugins_to_import:

    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)
        if not import_name:
            import_name,(plugin_path, mod, plugin_info) = plugins_to_import.popitem()
            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
            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"""
    def asyncConnect(self, profile_key = '@DEFAULT@'):
        """Connect to jabber server with asynchronous reply
        @param profile_key: %(doc_profile)s

        profile = self.memory.getProfileName(profile_key)
        if not profile:
            error (_('Trying to connect a non-exsitant profile'))
        if (self.isConnected(profile)):
            info(_("already connected !"))
            return defer.succeed("None")

        def afterMemoryInit(ignore):
            """This part must be called when we have loaded individual parameters from memory"""
            current = self.profiles[profile] = xmpp.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 = xmpp.SatMessageProtocol(self)
            current.roster = xmpp.SatRosterProtocol(self)

            current.presence = xmpp.SatPresenceProtocol(self)

            current.fallBack = xmpp.SatFallbackHandler(self)

            current.versionHandler = xmpp.SatVersionHandler(self.get_const('client_name'),

            debug (_("setting plugins parents"))
            for plugin in self.plugins.iteritems():
                if plugin[1].is_handler:


            return current.getConnectionDeferred()

        return self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit)

    def disconnect(self, profile_key):
        """disconnect from jabber server"""
        if (not self.isConnected(profile_key)):
            info(_("not connected !"))
        profile = self.memory.getProfileName(profile_key)

    def purgeClient(self, profile):
        """Remove reference to a profile client and purge cache
        the garbage collector can then free the memory"""
            del self.profiles[profile]
        except KeyError:
            error(_("Trying to remove reference to a client not referenced"))

    def startService(self):
        info("Salut à toi ô mon frère !")
        #TODO: manage autoconnect
    def stopService(self):
        info("Salut aussi à Rantanplan")

    def run(self):
        debug(_("running app"))
    def stop(self):
        debug(_("stopping app"))
    ## 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, email, 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(xmpp.RegisteringAuthenticator(self, server, login, password, email, 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.")})

        confirm_id = sat_next_id()
        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}},
        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, None, server, id=action_id)
            self.actionResult(action_id, "SUPPRESS", {})

    def submitForm(self, action, target, fields, profile_key):
        """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)
        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)
        elif action=='CANCEL':
            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):
        """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):
        """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'))
        if not self.profiles.has_key(profile):
            return False
        return self.profiles[profile].isConnected()

    def launchAction(self, type, data, profile_key):
        """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":
                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
            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)
        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)
        if type!="groupchat":
            self.memory.addToHistory(current_jid, jid.JID(to), unicode(msg), profile=profile) #we don't add groupchat message to history, as we get them back
                                                                                              #and they will we added then
            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)
        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):
        """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)
        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":
        elif subs_type=="subscribed":
        elif subs_type=="unsubscribe":
        elif subs_type=="unsubscribed":

    def addContact(self, to, profile_key):
        """Add a contact in roster list"""
        profile = self.memory.getProfileName(profile_key)
        #self.profiles[profile].roster.addItem(to_jid) XXX: disabled (cf

    def updateContact(self, to, name, groups, profile_key):
        """update a contact in roster list"""
        profile = self.memory.getProfileName(profile_key)
        to_jid = jid.JID(to)
        groups = set(groups)
        roster_item = RosterItem(to_jid) = name or None
        roster_item.groups = set(groups)

    def delContact(self, to, profile_key):
        """Remove contact from roster list"""
        profile = self.memory.getProfileName(profile_key)

    ## 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) 

        def _errback(result, entity, profile):
            warning(_("Can't get information on identity [%(entity)s] for profile [%(profile)s]") % {'entity':entity, 'profile': profile})
        defer_list = []
        for item in disco_result._items:
            if item.entity.full().count('.') == 1: #XXX: workaround for a bug on jabberfr, tmp
            args = [item.entity, profile] 
            defer_list.append(disco_client.requestInfo(item.entity).addCallbacks(_check_entity_cb, _errback, args, None, args))

    ## 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_TRANSFER")
        @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"))
            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"))
            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"))
            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 = {}
            self.__progress_cb_map[id](id, data)
        except KeyError:
            #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"))
            del self.__general_cb_map[name]

    def callGeneralCB(self, name, *args, **kwargs):
        """Call general function back"""
            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")
        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"""
            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
            error (_("Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)")%{'category':category, 'name':name,'type':type})
            return ""