view src/tools/memory.py @ 417:f1bf8b6143b7

core: removed former save_data method
author Goffi <goffi@goffi.org>
date Tue, 01 Nov 2011 23:04:48 +0100
parents 32dc8b18c2ae
children 199cf4ebcc74
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
SAT: a jabber client
Copyright (C) 2009, 2010, 2011  Jérôme Poisson (goffi@goffi.org)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

from __future__ import with_statement

import os.path
import time
import cPickle as pickle
from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError
from xml.dom import minidom
from logging import debug, info, warning, error
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from sat.tools.xml_tools import paramsXml2xmlUI
from sat.core.default_config import default_config
from sat.tools.sqlite import SqliteStorage

SAVEFILE_PARAM_XML="/param" #xml parameters template
SAVEFILE_PARAM_DATA="/param" #individual & general parameters; _ind and _gen suffixes will be added
SAVEFILE_HISTORY="/history"
SAVEFILE_PRIVATE="/private"  #file used to store misc values (mainly for plugins)
SAVEFILE_DATABASE="/sat.db"

class ProfileNotInCacheError(Exception):
    pass

class Param():
    """This class manage parameters with xml"""
    ### TODO: add desciption in params
    
    #TODO: move Watched in a plugin
    default_xml = u"""
    <params>
    <general>
    </general>
    <individual>
        <category name="Connection" label="%(category_connection)s">
            <param name="JabberID" value="name@example.org/SàT" type="string" />
            <param name="Password" value="" type="password" />
            <param name="Server" value="example.org" type="string" />
            <param name="NewAccount" value="%(label_NewAccount)s" type="button" callback_id="registerNewAccount"/>
            <param name="autoconnect" label="%(label_autoconnect)s" value="true" type="bool" />
            <param name="autodisconnect" label="%(label_autodisconnect)s" value="false"  type="bool" />
        </category>
        <category name="Misc" label="%(category_misc)s">
            <param name="Watched" value="test@Jabber.goffi.int" type="string" />
        </category>
    </individual>
    </params>
    """ % {'category_connection': _("Connection"),
           'label_NewAccount': _("Register new account"),
           'label_autoconnect': _('Connect on frontend startup'),
           'label_autodisconnect': _('Disconnect on frontend closure'),
           'category_misc': _("Misc")
          }

    def load_default_params(self):
        self.dom = minidom.parseString(Param.default_xml.encode('utf-8'))

    def load_xml(self, file):
        """Load parameters template from file"""
        self.dom = minidom.parse(file)
    
    def loadGenParams(self):
        """Load general parameters data from storage
        @return: deferred triggered once params are loaded"""
        return self.storage.loadGenParams(self.params_gen)
        
    def loadIndParams(self, profile):
        """Load individual parameters
        set self.params cache
        @param profile: profile to load (*must exist*)
        @return: deferred triggered once params are loaded"""
        self.params[profile] = {}
        return self.storage.loadIndParams(self.params, profile)
   
    def purgeProfile(self, profile):
        """Remove cache data of a profile
        @param profile: %(doc_profile)s"""
        try:
            del self.params[profile]
        except KeyError:
            error(_("Trying to purge cache of a profile not in memory: [%s]") % profile)

    def save_xml(self, file):
        """Save parameters template to xml file"""
        with open(file, 'wb') as xml_file:
            xml_file.write(self.dom.toxml('utf-8'))

    def __init__(self, host, storage):
        debug("Parameters init")
        self.host = host
        self.storage = storage
        self.default_profile = None
        self.params = {}
        self.params_gen = {}
        host.set_const('savefile_param_xml', SAVEFILE_PARAM_XML)
        host.set_const('savefile_param_data', SAVEFILE_PARAM_DATA)
        host.registerGeneralCB("registerNewAccount", host.registerNewAccountCB)

    def createProfile(self, name):
        """Create a new profile
        @param name: Name of the profile"""
        if self.storage.hasProfile(name):
            info (_('The profile name already exists'))
            return True
        if not self.host.trigger.point("ProfileCreation", name):
            return False
        self.params[name]={}
        return False

    def deleteProfile(self, name):
        """Delete an existing profile
        @param name: Name of the profile"""
        if not self.storage.hasProfile(name):
            error (_('Trying to delete an unknown profile'))
            return True
        del self.params[name]
        return False

    def getProfileName(self, profile_key):
        """return profile according to profile_key
        @param profile_key: profile name or key which can be
                            @ALL@ for all profiles
                            @DEFAULT@ for default profile
        @return: requested profile name or None if it doesn't exist"""
        if profile_key=='@DEFAULT@':
            if not self.params:
                return ""
            default = self.host.memory.getPrivate('Profile_default')
            if not default or not default in self.params:
                info(_('No default profile, returning first one')) #TODO: manage real default profile
                default = self.getProfilesList()[0]
                self.host.memory.setPrivate('Profile_default', default)
            return default #FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists
        if not self.storage.hasProfile(profile_key):
            info (_('Trying to access an unknown profile'))
            return ""
        return profile_key

    def __get_unique_node(self, parent, tag, name):
        """return node with given tag
        @param parent: parent of nodes to check (e.g. documentElement)
        @param tag: tag to check (e.g. "category")
        @param name: name to check (e.g. "JID")
        @return: node if it exist or None
        """
        for node in parent.childNodes:
            if node.nodeName == tag and node.getAttribute("name") == name:
                #the node already exists
                return node
        #the node is new
        return None

    def importParams(self, xml):
        """import xml in parameters, do nothing if the param already exist
        @param xml: parameters in xml form"""
        src_dom = minidom.parseString(xml.encode('utf-8'))

        def import_node(tgt_parent, src_parent):
            for child in src_parent.childNodes:
                if child.nodeName == '#text':
                    continue
                node = self.__get_unique_node(tgt_parent, child.nodeName, child.getAttribute("name"))
                if not node: #The node is new
                    tgt_parent.appendChild(child)
                else:
                    import_node(node, child)

        import_node(self.dom.documentElement, src_dom.documentElement)

    def __default_ok(self, value, name, category):
        #FIXME: gof: will not work with individual parameters
        self.setParam(name, value, category) #FIXME: better to set param xml value ???

    def __default_ko(self, failure, name, category):
        error (_("Can't determine default value for [%(category)s/%(name)s]: %(reason)s") % {'category':category, 'name':name, 'reason':str(failure.value)})

    def setDefault(self, name, category, callback, errback=None):
        """Set default value of parameter
        'default_cb' attibute of parameter must be set to 'yes'
        @param name: name of the parameter
        @param category: category of the parameter
        @param callback: must return a string with the value (use deferred if needed)
        @param errback: must manage the error with args failure, name, category
        """
        #TODO: send signal param update if value changed
        node =  self.__getParamNode(name, category, '@ALL@')
        if not node:
            error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name':name, 'category':category})
            return
        if node[1].getAttribute('default_cb') == 'yes':
            del node[1].attributes['default_cb']
            d = defer.maybeDeferred(callback)
            d.addCallback(self.__default_ok, name, category)
            d.addErrback(errback or self.__default_ko, name, category)

    def getParamA(self, name, category, attr="value", profile_key="@DEFAULT@"):
        """Helper method to get a specific attribute
           @param name: name of the parameter
           @param category: category of the parameter
           @param attr: name of the attribute (default: "value")
           @param profile: owner of the param (@ALL@ for everyone)
           
           @return: attribute"""
        node = self.__getParamNode(name, category)
        if not node:
            error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name':name, 'category':category})
            return ""

        if node[0] == 'general':
            value = self.__getParam(None, category, name, 'general')
            return value if value!=None else node[1].getAttribute(attr)
        
        assert(node[0] == 'individual')

        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Requesting a param for an non-existant profile'))
            return ""
        
        if profile not in self.params:
            error(_('Requesting synchronous param for not connected profile'))
            return ""

        if attr == "value": 
            value = self.__getParam(profile, category, name)
            return value if value!=None else node[1].getAttribute(attr)
        else:
            return node[1].getAttribute(attr)

    def asyncGetParamA(self, name, category, attr="value", profile_key="@DEFAULT@", callback=None, errback=None):
        """Helper method to get a specific attribute
           @param name: name of the parameter
           @param category: category of the parameter
           @param attr: name of the attribute (default: "value")
           @param profile: owner of the param (@ALL@ for everyone)
           @param callback: called when the profile is connected
           @param errback: called is the connection fail"""
        node = self.__getParamNode(name, category)
        if not node:
            error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name':name, 'category':category})
            return None

        if node[0] == 'general':
            value = self.__getParam(None, category, name, 'general')
            callback(value if value!=None else node[1].getAttribute(attr))
            return
        
        assert(node[0] == 'individual')

        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Requesting a param for a non-existant profile'))
            errback()
            return
        
        if attr != "value": 
            callback(node[1].getAttribute(attr))
            return
        default = node[1].getAttribute(attr)
        try:
            value = self.__getParam(profile, category, name)
            callback(value if value!=None else default)
        except ProfileNotInCacheError:
            #We have to ask data to the storage manager
            d = self.storage.getIndParam(category, name, profile)
            d.addCallback(lambda value: callback(value if value!=None else default))
            d.addErrback(lambda x:errback())

    def __getParam(self, profile, category, name, type='individual'):
        """Return the param, or None if it doesn't exist
        @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@)
        @param category: param category
        @param name: param name
        """
        if type == 'general':
            if self.params_gen.has_key((category, name)):
                return self.params_gen[(category, name)]
            return None  #This general param has the default value
        assert (type == 'individual')
        if not self.params.has_key(profile):
            raise ProfileNotInCacheError
        if not self.params[profile].has_key((category, name)):
            return None
        return self.params[profile][(category, name)]

    def __constructProfileXml(self, profile):
        """Construct xml for asked profile, filling values when needed
        /!\ as noticed in doc, don't forget to unlink the minidom.Document
        @param profile: profile name (not key !)
        @return: minidom.Document of the profile xml (cf warning above)
        """
        #TODO: asynchronous equivalent
        prof_xml = minidom.parseString('<params/>')
        cache = {}

        for type_node in self.dom.documentElement.childNodes:
            if type_node.nodeName == 'general' or type_node.nodeName == 'individual':  #we use all params, general and individual
                for cat_node in type_node.childNodes:
                    if cat_node.nodeName == 'category':
                        category = cat_node.getAttribute('name')
                        if not cache.has_key(category):
                            cache[category] = dest_cat = cat_node.cloneNode(True) #we make a copy for the new xml
                            new_node = True
                        else:
                            dest_cat = cache[category]
                            new_node = False #It's not a new node, we will merge information
                        params = cat_node.getElementsByTagName("param")
                        dest_params = {}
                        for node in dest_cat.childNodes:
                            if node.nodeName != "param":
                                continue
                            dest_params[node.getAttribute('name')] = node

                        for param_node in params:
                            name = param_node.getAttribute('name')
                            
                            if name not in dest_params:
                                dest_params[name] = param_node.cloneNode(True)
                                dest_cat.appendChild(dest_params[name])

                            profile_value = self.__getParam(profile, category, name, type_node.nodeName)
                            if profile_value!=None:  #there is a value for this profile, we must change the default
                                dest_params[name].setAttribute('value', profile_value)
                        if new_node:
                            prof_xml.documentElement.appendChild(dest_cat)
        return prof_xml


    def getParamsUI(self, profile_key):
        """Return a SàT XMLUI for parameters, with given profile"""
        #TODO: asynchronous equivalent
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_("Asking params for inexistant profile"))
            return ""
        param_xml = self.getParams(profile)
        return paramsXml2xmlUI(param_xml)

    def getParams(self, profile_key):
        """Construct xml for asked profile
        Take params xml as skeleton"""
        #TODO: asynchronous equivalent
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_("Asking params for inexistant profile"))
            return ""
        prof_xml = self.__constructProfileXml(profile) 
        return_xml = prof_xml.toxml()
        prof_xml.unlink()

        return return_xml

    def getParamsForCategory(self, category, profile_key):
        """Return node's xml for selected category"""
        #TODO: manage category of general type (without existant profile)
        #TODO: asynchronous equivalent
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_("Asking params for inexistant profile"))
            return ""
        prof_xml = self.__constructProfileXml(profile) 
        
        for node in prof_xml.getElementsByTagName("category"):
            if node.nodeName == "category" and node.getAttribute("name") == category:
                result = node.toxml()
                prof_xml.unlink()
                return result

        prof_xml.unlink()
        return "<category />"

    def __getParamNode(self, name, category, type="@ALL@"): #FIXME: is type useful ?
        """Return a node from the param_xml
        @param name: name of the node
        @param category: category of the node
        @type: keyword for search:
                                    @ALL@ search everywhere
                                    @GENERAL@ only search in general type
                                    @INDIVIDUAL@ only search in individual type
        @return: a tuple with the node type and the the node, or None if not found"""

        for type_node in self.dom.documentElement.childNodes:
            if ( ((type == "@ALL@" or type == "@GENERAL@") and type_node.nodeName == 'general') 
            or ( (type == "@ALL@" or type == "@INDIVIDUAL@") and type_node.nodeName == 'individual') ):
                for node in type_node.getElementsByTagName('category'):
                    if node.getAttribute("name") == category:
                        params = node.getElementsByTagName("param")
                        for param in params:
                            if param.getAttribute("name") == name:
                                return (type_node.nodeName, param)
        return None
        
    def getParamsCategories(self):
        """return the categories availables"""
        categories=[]
        for cat in self.dom.getElementsByTagName("category"):
            name = cat.getAttribute("name")
            if name not in categories:
                categories.append(cat.getAttribute("name"))
        return categories

    def setParam(self, name, value, category, profile_key='@NONE@'):
        """Set a parameter, return None if the parameter is not in param xml"""
        #TODO: use different behaviour depending of the data type (e.g. password encrypted)
        if profile_key!="@NONE@":
            profile = self.getProfileName(profile_key)
            if not profile:
                error(_('Trying to set parameter for an unknown profile'))
                return #TODO: throw an error

        node = self.__getParamNode(name, category, '@ALL@')
        if not node:
            error(_('Requesting an unknown parameter (%(category)s/%(name)s)') % {'category':category, 'name':name})
            return
        
        if node[0] == 'general':
            self.params_gen[(category, name)] = value
            self.storage.setGenParam(category, name, value)
            for profile in self.getProfilesList():
                if self.host.isConnected(profile):
                    self.host.bridge.paramUpdate(name, value, category, profile)
            return
        
        assert (node[0] == 'individual')
        assert (profile_key != "@NONE@")
        
        type = node[1].getAttribute("type")
        if type=="button":
            print "clique",node.toxml()
        else:
            if self.host.isConnected(profile): #key can not exists if profile is not connected
                self.params[profile][(category, name)] = value
            self.host.bridge.paramUpdate(name, value, category, profile)
            self.storage.setIndParam(category, name, value, profile)

class Memory:
    """This class manage all persistent informations"""

    def __init__(self, host):
        info (_("Memory manager init"))
        self.initialized = defer.Deferred() 
        init_defers = [] #list of deferred to wait before initialization is finished
        self.host = host
        self.contacts={}
        self.presenceStatus={}
        self.lastResource={} #tmp, will be refactored with bdd integration
        self.subscriptions={}
        self.history={}  #used to store chat history (key: short jid)
        self.private={}  #used to store private value
        self.server_features={} #used to store discovery's informations
        self.server_identities={}
        self.config = self.parseMainConf()
        host.set_const('savefile_history', SAVEFILE_HISTORY)
        host.set_const('savefile_private', SAVEFILE_PRIVATE)
        host.set_const('savefile_database', SAVEFILE_DATABASE)
        database_file = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_database'))
        self.storage = SqliteStorage(database_file)
        self.params=Param(host, self.storage)
        self.loadFiles()
        self.storage.initialized.addCallback(lambda ignore: self.load(init_defers))
        
        defer.DeferredList(init_defers).chainDeferred(self.initialized)

    def parseMainConf(self):
        """look for main .ini configuration file, and parse it"""
        _config = SafeConfigParser(defaults=default_config)
        try:
            _config.read(map(os.path.expanduser, ['/etc/sat.conf', '~/sat.conf', '~/.sat.conf', 'sat.conf', '.sat.conf']))
        except:
            error (_("Can't read main config !"))

        return _config

    def getConfig(self, section, name):
        """Get the main configuration option
        @param section: section of the config file (None or '' for DEFAULT)
        @param name: name of the option
        """
        if not section:
            section='DEFAULT'
        try:
            _value = self.config.get(section, name)
        except NoOptionError, NoSectionError:
            _value = ''

        return os.path.expanduser(_value) if name.endswith('_path') or name.endswith('_dir') else _value


    def loadFiles(self):
        """Load parameters and all memory things from file/db"""
        param_file_xml = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_param_xml'))
        param_file_data = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_param_data'))
        history_file = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_history'))
        private_file = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_private'))

        #parameters template
        if os.path.exists(param_file_xml):
            try:
                self.params.load_xml(param_file_xml)
                debug(_("params template loaded"))
            except:
                error (_("Can't load params template !"))
                self.params.load_default_params()
        else:
            info (_("No params template, using default template"))
            self.params.load_default_params()

        
        #history
        if os.path.exists(history_file):
            try:
                with open(history_file, 'r') as history_pickle:
                    self.history=pickle.load(history_pickle)
                debug(_("history loaded"))
            except:
                error (_("Can't load history !"))

        #private
        if os.path.exists(private_file):
            try:
                with open(private_file, 'r') as private_pickle:
                    self.private=pickle.load(private_pickle)
                debug(_("private values loaded"))
            except:
                error (_("Can't load private values !"))

    def load(self, init_defers):
        """Load parameters and all memory things from db
        @param init_defers: list of deferred to wait before parameters are loaded"""
        #parameters data
        init_defers.append(self.params.loadGenParams())

    def loadIndividualParams(self, profile):
        """Load individual parameters for a profile
        @param profile: %(doc_profile)s"""
        return self.params.loadIndParams(profile)

    def purgeProfile(self, profile):
        """Delete cache of data of profile
        @param profile: %(doc_profile)s"""
        self.params.purgeProfile(profile)

    def save(self):
        """Save parameters and all memory things to file/db"""
        #TODO: need to encrypt files (at least passwords !) and set permissions
        param_file_xml = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_param_xml'))
        param_file_data = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_param_data'))
        history_file = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_history'))
        private_file = os.path.expanduser(self.getConfig('','local_dir')+
                                        self.host.get_const('savefile_private'))
        
        self.params.save_xml(param_file_xml)
        debug(_("params saved"))
        with open(history_file, 'w') as history_pickle:
            pickle.dump(self.history, history_pickle)
        debug(_("history saved"))
        with open(private_file, 'w') as private_pickle:
            pickle.dump(self.private, private_pickle)
        debug(_("private values saved"))

    def getProfilesList(self):
        return self.storage.getProfilesList()


    def getProfileName(self, profile_key):
        """Return name of profile from keyword
        @param profile_key: can be the profile name or a keywork (like @DEFAULT@)
        @return: profile name or None if it doesn't exist"""
        return self.params.getProfileName(profile_key)

    def createProfile(self, name):
        """Create a new profile
        @param name: Profile name
        """
        return self.params.createProfile(name)
    
    def deleteProfile(self, name):
        """Delete an existing profile
        @param name: Name of the profile"""
        return self.params.deleteProfile(name)

    def addToHistory(self, me_jid, from_jid, to_jid, type, message):
        me_short=me_jid.userhost()
        from_short=from_jid.userhost()
        to_short=to_jid.userhost()

        if from_jid==me_jid:
            key=to_short
        else:
            key=from_short

        if not self.history.has_key(me_short):
            self.history[me_short]={}
        if not self.history[me_short].has_key(key):
            self.history[me_short][key]={}

        self.history[me_short][key][int(time.time())] = (from_jid.full(), message)
        
    def getHistory(self, from_jid, to_jid, size):
        ret={}
        if not self.history.has_key(from_jid):
            error(_("source JID not found !"))
            #TODO: throw an error here
            return {}
        if not self.history[from_jid].has_key(to_jid):
            error(_("dest JID not found !"))
            #TODO: throw an error here
            return {}
        stamps=self.history[from_jid][to_jid].keys()
        stamps.sort()
        for stamp in stamps[-size:]:
            ret[stamp]=self.history[from_jid][to_jid][stamp]

        return ret

    def setPrivate(self, key, value):
        """Save a misc private value (mainly useful for plugins)"""
        self.private[key] = value

    def getPrivate(self, key):
        """return a private value
        @param key: name of wanted value
        @return: value or None if value don't exist"""
        if self.private.has_key(key):
            return self.private[key]
        return None

    def addServerFeature(self, feature, profile):
        """Add a feature discovered from server
        @param feature: string of the feature
        @param profile: which profile is using this server ?"""
        if not self.server_features.has_key(profile):
            self.server_features[profile] = []
        self.server_features[profile].append(feature)
    
    def addServerIdentity(self, category, type, entity, profile):
        """Add an identity discovered from server
        @param feature: string of the feature
        @param profile: which profile is using this server ?"""
        if not self.server_identities.has_key(profile):
            self.server_identities[profile] = {}
        if not self.server_identities[profile].has_key((category, type)):
            self.server_identities[profile][(category, type)]=set()
        self.server_identities[profile][(category, type)].add(entity)

    def getServerServiceEntities(self, category, type, profile):
        """Return all available entities for a service"""
        if self.server_identities.has_key(profile):
            return self.server_identities[profile].get((category, type), set())
        else:
            return None

    def getServerServiceEntity(self, category, type, profile):
        """Helper method to get first available entity for a service"""
        entities = self.getServerServiceEntities(category, type, profile)
        if entities == None:
            warning(_("Entities not available, maybe they haven't been asked to server yet ?"))
            return None
        else:
            return list(entities)[0] if entities else None

    def hasServerFeature(self, feature, profile_key):
        """Tell if the server of the profile has the required feature"""
        profile = self.getProfileName(profile_key)
        if not profile:
            error (_('Trying find server feature for a non-existant profile'))
            return
        assert(self.server_features.has_key(profile))
        return feature in self.server_features[profile]


    def addContact(self, contact_jid, attributes, groups, profile_key):
        debug("Memory addContact: %s",contact_jid.userhost())
        profile = self.getProfileName(profile_key)
        if not profile:
            error (_('Trying to add a contact to a non-existant profile'))
            return
        assert(isinstance(attributes,dict))
        assert(isinstance(groups,set))
        if not self.contacts.has_key(profile):
            self.contacts[profile] = {}
        self.contacts[profile][contact_jid.userhost()]=[attributes, groups]

    def delContact(self, contact_jid, profile_key):
        debug("Memory delContact: %s",contact_jid.userhost())
        profile = self.getProfileName(profile_key)
        if not profile:
            error (_('Trying to delete a contact for a non-existant profile'))
            return
        if self.contacts.has_key(profile) and self.contacts[profile].has_key(contact_jid.userhost()):
            del self.contacts[profile][contact_jid.userhost()]
    
    def getContact(self, contact_jid, profile_key):
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Asking a contact for a non-existant profile'))
            return None
        if self.contacts.has_key(profile) and self.contacts[profile].has_key(contact_jid.userhost()):
            return self.contacts[profile][contact_jid.userhost()]
    
    def getContacts(self, profile_key):
        """Return list of contacts for given profile
        @param profile_key: profile key
        @return list of [contact, attr, groups]"""
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Asking contacts for a non-existant profile'))
            return []
        ret=[]
        if self.contacts.has_key(profile):
            for contact in self.contacts[profile]:
                attr, groups = self.contacts[profile][contact]
                ret.append([contact, attr, groups ])
        return ret

    def getLastResource(self, contact, profile_key):
        """Return the last resource used by a contact
        @param contact: contact jid (unicode)
        @param profile_key: %(doc_profile_key)s"""
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Asking contacts for a non-existant profile'))
            return ""
        try:
            return self.lastResource[profile][jid.JID(contact).userhost()]
        except:
            return ""
    
    def addPresenceStatus(self, contact_jid, show, priority, statuses, profile_key):
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Trying to add presence status to a non-existant profile'))
            return
        if not self.presenceStatus.has_key(profile):
            self.presenceStatus[profile] = {}
        if not self.lastResource.has_key(profile):
            self.lastResource[profile] = {}
        if not self.presenceStatus[profile].has_key(contact_jid.userhost()):
            self.presenceStatus[profile][contact_jid.userhost()] = {}
        resource = jid.parse(contact_jid.full())[2] or ''
        if resource:
            self.lastResource[profile][contact_jid.userhost()] = resource

        self.presenceStatus[profile][contact_jid.userhost()][resource] = (show, priority, statuses)

    def addWaitingSub(self, type, contact_jid, profile_key):
        """Called when a subcription request is received"""
        profile = self.getProfileName(profile_key)
        assert(profile)
        if not self.subscriptions.has_key(profile):
            self.subscriptions[profile] = {}
        self.subscriptions[profile][contact_jid] = type
    
    def delWaitingSub(self, contact_jid, profile_key):
        """Called when a subcription request is finished"""
        profile = self.getProfileName(profile_key)
        assert(profile)
        if self.subscriptions.has_key(profile) and self.subscriptions[profile].has_key(contact_jid):
            del self.subscriptions[profile][contact_jid]
    
    def getWaitingSub(self, profile_key):
        """Called to get a list of currently waiting subscription requests"""
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Asking waiting subscriptions for a non-existant profile'))
            return {}
        if not self.subscriptions.has_key(profile):
            return {}
        
        return self.subscriptions[profile]

    def getPresenceStatus(self, profile_key):
        profile = self.getProfileName(profile_key)
        if not profile:
            error(_('Asking contacts for a non-existant profile'))
            return {}
        if not self.presenceStatus.has_key(profile):
            self.presenceStatus[profile] = {}
        debug ("Memory getPresenceStatus (%s)", self.presenceStatus[profile])
        return self.presenceStatus[profile]

    def getParamA(self, name, category, attr="value", profile_key='@DEFAULT@'):
        return self.params.getParamA(name, category, attr, profile_key)
    
    def asyncGetParamA(self, name, category, attr="value", profile_key='@DEFAULT@', callback=None, errback=None):
        return self.params.asyncGetParamA(name, category, attr, profile_key, callback, errback)
    
    def getParamsUI(self, profile_key):
        return self.params.getParamsUI(profile_key)
  
    def getParams(self, profile_key):
        return self.params.getParams(profile_key) 
    
    def getParamsForCategory(self, category, profile_key):
        return self.params.getParamsForCategory(category, profile_key) 
    
    def getParamsCategories(self):
        return self.params.getParamsCategories()
    
    def setParam(self, name, value, category, profile_key):
        return self.params.setParam(name, value, category, profile_key)

    def importParams(self, xml):
        return self.params.importParams(xml)
    
    def setDefault(self, name, category, callback, errback=None):
        return self.params.setDefault(name, category, callback, errback)