diff src/memory/memory.py @ 1367:f71a0fc26886

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 10:52:28 +0100
parents be3a301540c0
children 3a20312d4012
line wrap: on
line diff
--- a/src/memory/memory.py	Thu Feb 05 11:59:26 2015 +0100
+++ b/src/memory/memory.py	Wed Mar 18 10:52:28 2015 +0100
@@ -19,11 +19,14 @@
 
 from sat.core.i18n import _
 
+from sat.core.log import getLogger
+log = getLogger(__name__)
+
 import os.path
+import copy
+from collections import namedtuple
 from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError
 from uuid import uuid4
-from sat.core.log import getLogger
-log = getLogger(__name__)
 from twisted.internet import defer, reactor
 from twisted.words.protocols.jabber import jid
 from sat.core import exceptions
@@ -36,6 +39,8 @@
 from sat.tools import config as tools_config
 
 
+PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses'))
+
 class Sessions(object):
     """Sessions are data associated to key used for a temporary moment, with optional profile checking."""
     DEFAULT_TIMEOUT = 600
@@ -212,6 +217,9 @@
         self.host = host
         self._entities_cache = {} # XXX: keep presence/last resource/other data in cache
                                   #     /!\ an entity is not necessarily in roster
+                                  #     main key is bare jid, value is a dict
+                                  #     where main key is resource, or None for bare jid
+        self._key_signals = set() # key which need a signal to frontends when updated
         self.subscriptions = {}
         self.auth_sessions = PasswordSessions()  # remember the authenticated profiles
         self.disco = Discovery(host)
@@ -228,6 +236,8 @@
         d.addCallback(lambda ignore: self.memory_data.load())
         d.chainDeferred(self.initialized)
 
+    ## Configuration ##
+
     def parseMainConf(self):
         """look for main .ini configuration file, and parse it"""
         config = SafeConfigParser(defaults=C.DEFAULT_CONFIG)
@@ -265,6 +275,24 @@
                 log.error(_("Can't load parameters from file: %s") % e)
         return False
 
+    def save_xml(self, filename):
+        """Save parameters template to xml file
+
+        @param filename (str): output file
+        @return: bool: True in case of success
+        """
+        if not filename:
+            return False
+        #TODO: need to encrypt files (at least passwords !) and set permissions
+        filename = os.path.expanduser(filename)
+        try:
+            self.params.save_xml(filename)
+            log.debug(_("Parameters saved to file: %s") % filename)
+            return True
+        except Exception as e:
+            log.error(_("Can't save parameters to file: %s") % e)
+        return False
+
     def load(self):
         """Load parameters and all memory things from db"""
         #parameters data
@@ -275,6 +303,8 @@
         @param profile: %(doc_profile)s"""
         return self.params.loadIndParams(profile)
 
+    ## Profiles/Sessions management ##
+
     def startProfileSession(self, profile):
         """"Iniatialise session for a profile
         @param profile: %(doc_profile)s"""
@@ -309,31 +339,16 @@
         except KeyError:
             log.error(_("Trying to purge roster status cache for a profile not in memory: [%s]") % profile)
 
-    def save_xml(self, filename):
-        """Save parameters template to xml file
-
-        @param filename (str): output file
-        @return: bool: True in case of success
-        """
-        if not filename:
-            return False
-        #TODO: need to encrypt files (at least passwords !) and set permissions
-        filename = os.path.expanduser(filename)
-        try:
-            self.params.save_xml(filename)
-            log.debug(_("Parameters saved to file: %s") % filename)
-            return True
-        except Exception as e:
-            log.error(_("Can't save parameters to file: %s") % e)
-        return False
-
     def getProfilesList(self):
         return self.storage.getProfilesList()
 
     def getProfileName(self, profile_key, return_profile_keys=False):
         """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"""
+        @param return_profile_keys: if True, return unmanaged profile keys (like "@ALL@"). This keys must be managed by the caller
+        @return: requested profile name or emptry string if it doesn't exist
+        """
         return self.params.getProfileName(profile_key, return_profile_keys)
 
     def asyncCreateProfile(self, name, password=''):
@@ -342,6 +357,10 @@
         @param password: profile password
         @return: Deferred
         """
+        if not name:
+            raise ValueError("Empty profile name")
+        if name[0] == '@':
+            raise ValueError("A profile name can't start with a '@'")
         personal_key = BlockCipher.getRandomKey(base64=True)  # generated once for all and saved in a PersistentDict
         self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=name)  # will be encrypted by setParam
         d = self.params.asyncCreateProfile(name)
@@ -358,6 +377,8 @@
         self.auth_sessions.profileDelUnique(name)
         return self.params.asyncDeleteProfile(name, force)
 
+    ## History ##
+
     def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile=C.PROF_KEY_NONE):
         assert profile != C.PROF_KEY_NONE
         if extra is None:
@@ -386,163 +407,305 @@
             return defer.succeed([])
         return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile)
 
-    def _getLastResource(self, jid_s, profile_key):
-        jid_ = jid.JID(jid_s)
-        return self.getLastResource(jid_, profile_key) or ""
-
-    def getLastResource(self, entity_jid, profile_key):
-        """Return the last resource used by an entity
-        @param entity_jid: entity jid
-        @param profile_key: %(doc_profile_key)s"""
-        data = self.getEntityData(entity_jid.userhostJID(), [C.ENTITY_LAST_RESOURCE], profile_key)
-        try:
-            return data[C.ENTITY_LAST_RESOURCE]
-        except KeyError:
-            return None
+    ## Statuses ##
 
     def _getPresenceStatuses(self, profile_key):
         ret = self.getPresenceStatuses(profile_key)
         return {entity.full():data for entity, data in ret.iteritems()}
 
     def getPresenceStatuses(self, profile_key):
-        """Get all the presence status of a profile
+        """Get all the presence statuses of a profile
+
         @param profile_key: %(doc_profile_key)s
         @return: presence data: key=entity JID, value=presence data for this entity
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
+        profile_cache = self._getProfileCache(profile_key)
         entities_presence = {}
-        for entity in self._entities_cache[profile]:
-            if "presence" in self._entities_cache[profile][entity]:
-                entities_presence[entity] = self._entities_cache[profile][entity]["presence"]
 
-        log.debug("Memory getPresenceStatus (%s)" % entities_presence)
+        for entity_jid, entity_data in profile_cache.iteritems():
+            for resource, resource_data in entity_data.iteritems():
+                full_jid = copy.copy(entity_jid)
+                full_jid.resource = resource
+                try:
+                    presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
+                except KeyError:
+                    continue
+                entities_presence.setdefault(entity_jid, {})[resource or ''] = presence_data
+
         return entities_presence
 
-    def isContactConnected(self, entity_jid, profile_key):
-        """Tell from the presence information if the given contact is connected.
+    def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
+        """Change the presence status of an entity
+
+        @param entity_jid: jid.JID of the entity
+        @param show: show status
+        @param priority: priority
+        @param statuses: dictionary of statuses
+        @param profile_key: %(doc_profile_key)s
+        """
+        presence_data = PresenceTuple(show, priority, statuses)
+        self.updateEntityData(entity_jid, "presence", presence_data, profile_key=profile_key)
+        if entity_jid.resource and show != C.PRESENCE_UNAVAILABLE:
+            # If a resource is available, bare jid should not have presence information
+            try:
+                self.delEntityDatum(entity_jid.userhostJID(), "presence", profile_key)
+            except (KeyError, exceptions.UnknownEntityError):
+                pass
+
+    ## Resources ##
+
+    def _getAllResource(self, jid_s, profile_key):
+        jid_ = jid.JID(jid_s)
+        return self.getAllResources(jid_, profile_key)
+
+    def getAllResources(self, entity_jid, profile_key):
+        """Return all resource from jid for which we have had data in this session
+
+        @param entity_jid: bare jid of the entit
+        @param profile_key: %(doc_profile_key)s
+        return (list[unicode]): list of resources
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        """
+        if entity_jid.resource:
+            raise ValueError("getAllResources must be used with a bare jid (got {})".format(entity_jid))
+        profile_cache = self._getProfileCache(profile_key)
+        try:
+            entity_data = profile_cache[entity_jid.userhostJID()]
+        except KeyError:
+            raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
+        resources= set(entity_data.keys())
+        resources.discard(None)
+        return resources
+
+    def getAvailableResources(self, entity_jid, profile_key):
+        """Return available resource for entity_jid
+
+        This method differs from getAllResources by returning only available resources
+        @param entity_jid: bare jid of the entit
+        @param profile_key: %(doc_profile_key)s
+        return (list[unicode]): list of available resources
 
-        @param entity_jid (JID): the entity to check
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        """
+        available = []
+        for resource in self.getAllResources(entity_jid, profile_key):
+            full_jid = copy.copy(entity_jid)
+            full_jid.resource = resource
+            try:
+                presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
+            except KeyError:
+                log.debug("Can't get presence data for {}".format(full_jid))
+            else:
+                if presence_data.show != C.PRESENCE_UNAVAILABLE:
+                    available.append(resource)
+        return available
+
+    def _getMainResource(self, jid_s, profile_key):
+        jid_ = jid.JID(jid_s)
+        return self.getMainResource(jid_, profile_key) or ""
+
+    def getMainResource(self, entity_jid, profile_key):
+        """Return the main resource used by an entity
+
+        @param entity_jid: bare entity jid
         @param profile_key: %(doc_profile_key)s
-        @return: boolean
+        @return (unicode): main resource or None
+        """
+        if entity_jid.resource:
+            raise ValueError("getMainResource must be used with a bare jid (got {})".format(entity_jid))
+        resources = self.getAllResources(entity_jid, profile_key)
+        priority_resources = []
+        for resource in resources:
+            full_jid = copy.copy(entity_jid)
+            full_jid.resource = resource
+            try:
+                presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
+            except KeyError:
+                log.debug("No presence information for {}".format(full_jid))
+                continue
+            priority_resources.append((resource, presence_data.priority))
+        try:
+            return max(priority_resources, key=lambda res_tuple: res_tuple[1])[0]
+        except ValueError:
+            log.warning("No resource found at all for {}".format(entity_jid))
+            return None
+
+    ## Entities data ##
+
+    def _getProfileCache(self, profile_key):
+        """Check profile validity and return its cache
+
+        @param profile: %(doc_profile_key)_s
+        @return (dict): profile cache
+
+        @raise exceptions.ProfileUnknownError: if profile doesn't exist
+        @raise exceptions.ProfileNotInCacheError: if there is no cache for this profile
         """
         profile = self.getProfileName(profile_key)
         if not profile:
             raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
         try:
-            presence = self._entities_cache[profile][entity_jid]['presence']
-            return len([True for status in presence.values() if status[0] != 'unavailable']) > 0
+            profile_cache = self._entities_cache[profile]
         except KeyError:
-            return False
+            raise exceptions.ProfileNotInCacheError
+        return profile_cache
+
+    def setSignalOnUpdate(self, key, signal=True):
+        """Set a signal flag on the key
+
+        When the key will be updated, a signal will be sent to frontends
+        @param key: key to signal
+        @param signal(boolean): if True, do the signal
+        """
+        if signal:
+            self._key_signals.add(key)
+        else:
+            self._key_signals.discard(key)
+
+    def getAllEntitiesIter(self, with_bare=False, profile_key=C.PROF_KEY_NONE):
+        """Return an iterator of full jids of all entities in cache
 
-    def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
-        """Change the presence status of an entity
-        @param entity_jid: jid.JID of the entity
-        @param show: show status
-        @param priotity: priotity
-        @param statuses: dictionary of statuses
+        @param with_bare: if True, include bare jids
+        @param profile_key: %(doc_profile_key)s
+        @return (list[unicode]): list of jids
+        """
+        profile_cache = self._getProfileCache(profile_key)
+        # we construct a list of all known full jids (bare jid of entities x resources)
+        for bare_jid, entity_data in profile_cache.iteritems():
+            for resource in entity_data.iterkeys():
+                if resource is None:
+                    continue
+                full_jid = copy.copy(bare_jid)
+                full_jid.resource = resource
+                yield full_jid
+
+    def updateEntityData(self, entity_jid, key, value, silent=False, profile_key=C.PROF_KEY_NONE):
+        """Set a misc data for an entity
+
+        If key was registered with setSignalOnUpdate, a signal will be sent to frontends
+        @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
+                           C.ENTITY_ALL for all entities (all resources + bare jids)
+        @param key: key to set (eg: "type")
+        @param value: value for this key (eg: "chatroom")
+        @param silent(bool): if True, doesn't send signal to frontend, even there is a signal flag (see setSignalOnUpdate)
         @param profile_key: %(doc_profile_key)s
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
-        resource = entity_jid.resource
-        if resource:
-            try:
-                type_ = self.getEntityDatum(entity_jid.userhostJID(), 'type', profile)
-            except KeyError:
-                type_ = 'contact'
-            if type_ != 'chatroom':
-                self.updateEntityData(entity_jid.userhostJID(), C.ENTITY_LAST_RESOURCE, resource, profile)
-        entity_data.setdefault("presence", {})[resource or ''] = (show, priority, statuses)
+        profile_cache = self._getProfileCache(profile_key)
+        if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
+            entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
+        else:
+            entities = (entity_jid,)
 
-    def _getEntitiesData(self, entity_jid, profile):
-        """Get data dictionary for entities
-        @param entity_jid: JID of the entity, or C.ENTITY_ALL for all entities)
-        @param profile: %(doc_profile)s
-        @return: entities_data (key=jid, value=entity_data)
-        @raise: exceptions.ProfileNotInCacheError if profile is not in cache
-        """
-        if not profile in self._entities_cache:
-            raise exceptions.ProfileNotInCacheError
-        if entity_jid == C.ENTITY_ALL:
-            entities_data = self._entities_cache[profile]
-        else:
-            entity_data = self._entities_cache[profile].setdefault(entity_jid, {})
-            entities_data = {entity_jid: entity_data}
-        return entities_data
+        for jid_ in entities:
+            entity_data = profile_cache.setdefault(jid_.userhostJID(),{}).setdefault(jid_.resource, {})
 
-    def _updateEntityResources(self, entity_jid, profile):
-        """Add a known resource to bare entity_jid cache
-        @param entity_jid: full entity_jid (with resource)
-        @param profile: %(doc_profile)s
-        """
-        assert(entity_jid.resource)
-        entity_data = self._getEntitiesData(entity_jid.userhostJID(), profile)[entity_jid.userhostJID()]
-        resources = entity_data.setdefault('resources', set())
-        resources.add(entity_jid.resource)
+            entity_data[key] = value
+            if key in self._key_signals and not silent:
+                if not isinstance(value, basestring):
+                    log.error(u"Setting a non string value ({}) for a key ({}) which has a signal flag".format(value, key))
+                else:
+                    self.host.bridge.entityDataUpdated(jid_.full(), key, value, self.getProfileName(profile_key))
 
-    def updateEntityData(self, entity_jid, key, value, profile_key):
-        """Set a misc data for an entity
-        @param entity_jid: JID of the entity, or C.ENTITY_ALL to update all entities)
-        @param key: key to set (eg: "type")
-        @param value: value for this key (eg: "chatroom")
-        @param profile_key: %(doc_profile_key)s
-        """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        entities_data = self._getEntitiesData(entity_jid, profile)
-        if entity_jid != C.ENTITY_ALL and entity_jid.resource:
-            self._updateEntityResources(entity_jid, profile)
+    def delEntityDatum(self, entity_jid, key, profile_key):
+        """Delete a data for an entity
 
-        for jid_ in entities_data:
-            entity_data = entities_data[jid_]
-            if value == C.PROF_KEY_NONE and key in entity_data:
-                del entity_data[key]
-            else:
-                entity_data[key] = value
-            if isinstance(value, basestring):
-                self.host.bridge.entityDataUpdated(jid_.full(), key, value, profile)
-
-    def delEntityData(self, entity_jid, key, profile_key):
-        """Delete data for an entity
-        @param entity_jid: JID of the entity, or C.ENTITY_ALL to delete data from all entities)
+        @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
+                           C.ENTITY_ALL for all entities (all resources + bare jids)
         @param key: key to delete (eg: "type")
         @param profile_key: %(doc_profile_key)s
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        @raise KeyError: key is not in cache
         """
-        entities_data = self._getEntitiesData(entity_jid, profile_key)
-        for entity_jid in entities_data:
-            entity_data = entities_data[entity_jid]
+        profile_cache = self._getProfileCache(profile_key)
+        if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
+            entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
+        else:
+            entities = (entity_jid,)
+
+        for jid_ in entities:
+            try:
+                entity_data = profile_cache[jid_.userhostJID()][jid_.resource]
+            except KeyError:
+                raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(jid_))
             try:
                 del entity_data[key]
-            except KeyError:
-                log.debug("Key [%s] doesn't exist for [%s] in entities_cache" % (key, entity_jid.full()))
+            except KeyError as e:
+                if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
+                    continue # we ignore KeyError when deleting keys from several entities
+                else:
+                    raise e
 
-    def getEntityData(self, entity_jid, keys_list, profile_key):
-        """Get a list of cached values for entity
+    def _getEntitiesData(self, entities_jids, keys_list, profile_key):
+        ret = self.getEntitiesData([jid.JID(jid_) for jid_ in entities_jids], keys_list, profile_key)
+        return {jid_.full(): data for jid_, data in ret.iteritems()}
 
-        @param entity_jid: JID of the entity
-        @param keys_list (iterable): list of keys to get, empty list for everything
+    def getEntitiesData(self, entities_jids, keys_list=None, profile_key=C.PROF_KEY_NONE):
+        """Get a list of cached values for several entities at once
+
+        @param entities_jids: jids of the entities, or empty list for all entities in cache
+        @param keys_list (iterable,None): list of keys to get, None for everything
         @param profile_key: %(doc_profile_key)s
         @return: dict withs values for each key in keys_list.
                  if there is no value of a given key, resulting dict will
                  have nothing with that key nether
-        @raise: exceptions.UnknownEntityError if entity is not in cache
+                 if an entity doesn't exist in cache, it will not appear
+                 in resulting dict
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
-        if not keys_list:
+        def fillEntityData(entity_cache_data):
+            entity_data = {}
+            if keys_list is None:
+                entity_data = entity_cache_data
+            else:
+                for key in keys_list:
+                    try:
+                        entity_data[key] = entity_cache_data[key]
+                    except KeyError:
+                        continue
             return entity_data
-        ret = {}
-        for key in keys_list:
-            if key in entity_data:
-                ret[key] = entity_data[key]
-        return ret
+
+        profile_cache = self._getProfileCache(profile_key)
+        ret_data = {}
+        if entities_jids:
+            for entity in entities_jids:
+                try:
+                    entity_cache_data = profile_cache[entity.userhostJID()][entity.resource]
+                except KeyError:
+                    continue
+                ret_data[entity.full()] = fillEntityData(entity_cache_data, keys_list)
+        else:
+            for bare_jid, data in profile_cache.iteritems():
+                for resource, entity_cache_data in data.iteritems():
+                    full_jid = copy.copy(bare_jid)
+                    full_jid.resource = resource
+                    ret_data[full_jid] = fillEntityData(entity_cache_data)
+
+        return ret_data
+
+    def getEntityData(self, entity_jid, keys_list=None, profile_key=C.PROF_KEY_NONE):
+        """Get a list of cached values for entity
+
+        @param entity_jid: JID of the entity
+        @param keys_list (iterable,None): list of keys to get, None for everything
+        @param profile_key: %(doc_profile_key)s
+        @return: dict withs values for each key in keys_list.
+                 if there is no value of a given key, resulting dict will
+                 have nothing with that key nether
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        """
+        profile_cache = self._getProfileCache(profile_key)
+        try:
+            entity_data = profile_cache[entity_jid.userhostJID()][entity_jid.resource]
+        except KeyError:
+            raise exceptions.UnknownEntityError(u"Entity {} not in cache (was requesting {})".format(entity_jid, keys_list))
+        if keys_list is None:
+            return entity_data
+
+        return {key: entity_data[key] for key in keys_list if key in entity_data}
 
     def getEntityDatum(self, entity_jid, key, profile_key):
         """Get a datum from entity
@@ -552,34 +715,36 @@
         @param profile_key: %(doc_profile_key)s
         @return: requested value
 
-        @raise: exceptions.UnknownEntityError if entity is not in cache
-        @raise: KeyError if there is no value for this key and this entity
+        @raise exceptions.UnknownEntityError: if entity is not in cache
+        @raise KeyError: if there is no value for this key and this entity
         """
         return self.getEntityData(entity_jid, (key,), profile_key)[key]
 
     def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE):
-        """Remove cached data for entity
+        """Remove all cached data for entity
+
         @param entity_jid: JID of the entity to delete
-        @param delete_all_resources: if True also delete all known resources form cache
+        @param delete_all_resources: if True also delete all known resources from cache (a bare jid must be given in this case)
         @param profile_key: %(doc_profile_key)s
+
+        @raise exceptions.UnknownEntityError: if entity is not in cache
         """
-        profile = self.getProfileName(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
-        to_delete = set([entity_jid])
+        profile_cache = self._getProfileCache(profile_key)
 
         if delete_all_resources:
             if entity_jid.resource:
                 raise ValueError(_("Need a bare jid to delete all resources"))
-            entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
-            resources = entity_data.setdefault('resources', set())
-            to_delete.update([jid.JID("%s/%s" % (entity_jid.userhost(), resource)) for resource in resources])
+            try:
+                del profile_cache[entity_jid]
+            except KeyError:
+                raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
+        else:
+            try:
+                del profile_cache[entity_jid.userhostJID()][entity_jid.resource]
+            except KeyError:
+                raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
 
-        for entity in to_delete:
-            try:
-                del self._entities_cache[profile][entity]
-            except KeyError:
-                log.debug("Can't delete entity [%s]: not in cache" % entity.full())
+    ## Encryption ##
 
     def encryptValue(self, value, profile):
         """Encrypt a value for the given profile. The personal key must be loaded
@@ -637,6 +802,8 @@
         d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
         return d.addCallback(gotIndMemory).addCallback(done)
 
+    ## Subscription requests ##
+
     def addWaitingSub(self, type_, entity_jid, profile_key):
         """Called when a subcription request is received"""
         profile = self.getProfileName(profile_key)
@@ -663,6 +830,8 @@
 
         return self.subscriptions[profile]
 
+    ## Parameters ##
+
     def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
         return self.params.getStringParamA(name, category, attr, profile_key)
 
@@ -698,3 +867,21 @@
 
     def setDefault(self, name, category, callback, errback=None):
         return self.params.setDefault(name, category, callback, errback)
+
+    ## Misc ##
+
+    def isEntityAvailable(self, entity_jid, profile_key):
+        """Tell from the presence information if the given entity is available.
+
+        @param entity_jid (JID): the entity to check (if bare jid is used, all resources are tested)
+        @param profile_key: %(doc_profile_key)s
+        @return (bool): True if entity is available
+        """
+        if not entity_jid.resource:
+            return bool(self.getAvailableResources) # is any resource is available, entity is available
+        try:
+            presence_data = self.getEntityDatum(entity_jid, "presence", profile_key)
+        except KeyError:
+            log.debug("No presence information for {}".format(entity_jid))
+            return False
+        return presence_data.show != C.PRESENCE_UNAVAILABLE