comparison src/memory/memory.py @ 1290:faa1129559b8 frontends_multi_profiles

core, frontends: refactoring to base Libervia on QuickFrontend (big mixed commit): /!\ not finished, everything is still instable ! - bridge: DBus bridge has been modified to allow blocking call to be called in the same way as asynchronous calls - bridge: calls with a callback and no errback are now possible, default errback log the error - constants: removed hack to manage presence without OrderedDict, as an OrderedDict like class has been implemented in Libervia - core: getLastResource has been removed and replaced by getMainResource (there is a global better management of resources) - various style improvments: use of constants when possible, fixed variable overlaps, import of module instead of direct class import - frontends: printInfo and printMessage methods in (Quick)Chat are more generic (use of extra instead of timestamp) - frontends: bridge creation and option parsing (command line arguments) are now specified by the frontend in QuickApp __init__ - frontends: ProfileManager manage a more complete plug sequence (some stuff formerly manage in contact_list have moved to ProfileManager) - quick_frontend (quick_widgets): QuickWidgetsManager is now iterable (all widgets are then returned), or can return an iterator on a specific class (return all widgets of this class) with getWidgets - frontends: tools.jid can now be used in Pyjamas, with some care - frontends (XMLUI): profile is now managed - core (memory): big improvment on entities cache management (and specially resource management) - core (params/exceptions): added PermissionError - various fixes and improvments, check diff for more details
author Goffi <goffi@goffi.org>
date Sat, 24 Jan 2015 01:00:29 +0100
parents cfd636203e8f
children bb9c32249778
comparison
equal deleted inserted replaced
1289:653f2e2eea31 1290:faa1129559b8
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 21
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24
22 import os.path 25 import os.path
26 import copy
27 from collections import namedtuple
23 from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError 28 from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError
24 from uuid import uuid4 29 from uuid import uuid4
25 from sat.core.log import getLogger
26 log = getLogger(__name__)
27 from twisted.internet import defer, reactor 30 from twisted.internet import defer, reactor
28 from twisted.words.protocols.jabber import jid 31 from twisted.words.protocols.jabber import jid
29 from sat.core import exceptions 32 from sat.core import exceptions
30 from sat.core.constants import Const as C 33 from sat.core.constants import Const as C
31 from sat.memory.sqlite import SqliteStorage 34 from sat.memory.sqlite import SqliteStorage
33 from sat.memory.params import Params 36 from sat.memory.params import Params
34 from sat.memory.disco import Discovery 37 from sat.memory.disco import Discovery
35 from sat.memory.crypto import BlockCipher 38 from sat.memory.crypto import BlockCipher
36 from sat.tools import config as tools_config 39 from sat.tools import config as tools_config
37 40
41
42 PresenceTuple = namedtuple("PresenceTuple", ('show', 'priority', 'statuses'))
38 43
39 class Sessions(object): 44 class Sessions(object):
40 """Sessions are data associated to key used for a temporary moment, with optional profile checking.""" 45 """Sessions are data associated to key used for a temporary moment, with optional profile checking."""
41 DEFAULT_TIMEOUT = 600 46 DEFAULT_TIMEOUT = 600
42 47
210 log.info(_("Memory manager init")) 215 log.info(_("Memory manager init"))
211 self.initialized = defer.Deferred() 216 self.initialized = defer.Deferred()
212 self.host = host 217 self.host = host
213 self._entities_cache = {} # XXX: keep presence/last resource/other data in cache 218 self._entities_cache = {} # XXX: keep presence/last resource/other data in cache
214 # /!\ an entity is not necessarily in roster 219 # /!\ an entity is not necessarily in roster
220 # main key is bare jid, value is a dict
221 # where main key is resource, or None for bare jid
222 self._key_signals = set() # key which need a signal to frontends when updated
215 self.subscriptions = {} 223 self.subscriptions = {}
216 self.auth_sessions = PasswordSessions() # remember the authenticated profiles 224 self.auth_sessions = PasswordSessions() # remember the authenticated profiles
217 self.disco = Discovery(host) 225 self.disco = Discovery(host)
218 fixLocalDir(False) # XXX: tmp update code, will be removed in the future 226 fixLocalDir(False) # XXX: tmp update code, will be removed in the future
219 self.config = self.parseMainConf() 227 self.config = self.parseMainConf()
226 d = self.storage.initialized.addCallback(lambda ignore: self.load()) 234 d = self.storage.initialized.addCallback(lambda ignore: self.load())
227 self.memory_data = PersistentDict("memory") 235 self.memory_data = PersistentDict("memory")
228 d.addCallback(lambda ignore: self.memory_data.load()) 236 d.addCallback(lambda ignore: self.memory_data.load())
229 d.chainDeferred(self.initialized) 237 d.chainDeferred(self.initialized)
230 238
239 ## Configuration ##
240
231 def parseMainConf(self): 241 def parseMainConf(self):
232 """look for main .ini configuration file, and parse it""" 242 """look for main .ini configuration file, and parse it"""
233 config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) 243 config = SafeConfigParser(defaults=C.DEFAULT_CONFIG)
234 try: 244 try:
235 config.read(C.CONFIG_FILES) 245 config.read(C.CONFIG_FILES)
263 return True 273 return True
264 except Exception as e: 274 except Exception as e:
265 log.error(_("Can't load parameters from file: %s") % e) 275 log.error(_("Can't load parameters from file: %s") % e)
266 return False 276 return False
267 277
268 def load(self):
269 """Load parameters and all memory things from db"""
270 #parameters data
271 return self.params.loadGenParams()
272
273 def loadIndividualParams(self, profile):
274 """Load individual parameters for a profile
275 @param profile: %(doc_profile)s"""
276 return self.params.loadIndParams(profile)
277
278 def startProfileSession(self, profile):
279 """"Iniatialise session for a profile
280 @param profile: %(doc_profile)s"""
281 log.info(_("[%s] Profile session started" % profile))
282 self._entities_cache[profile] = {}
283
284 def newAuthSession(self, key, profile):
285 """Start a new session for the authenticated profile.
286
287 The personal key is loaded encrypted from a PersistentDict before being decrypted.
288
289 @param key: the key to decrypt the personal key
290 @param profile: %(doc_profile)s
291 @return: a deferred None value
292 """
293 def gotPersonalKey(personal_key):
294 """Create the session for this profile and store the personal key"""
295 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=profile)
296 log.debug('auth session created for profile %s' % profile)
297
298 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
299 d.addCallback(lambda data: BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY]))
300 return d.addCallback(gotPersonalKey)
301
302 def purgeProfileSession(self, profile):
303 """Delete cache of data of profile
304 @param profile: %(doc_profile)s"""
305 log.info(_("[%s] Profile session purge" % profile))
306 self.params.purgeProfile(profile)
307 try:
308 del self._entities_cache[profile]
309 except KeyError:
310 log.error(_("Trying to purge roster status cache for a profile not in memory: [%s]") % profile)
311
312 def save_xml(self, filename): 278 def save_xml(self, filename):
313 """Save parameters template to xml file 279 """Save parameters template to xml file
314 280
315 @param filename (str): output file 281 @param filename (str): output file
316 @return: bool: True in case of success 282 @return: bool: True in case of success
324 log.debug(_("Parameters saved to file: %s") % filename) 290 log.debug(_("Parameters saved to file: %s") % filename)
325 return True 291 return True
326 except Exception as e: 292 except Exception as e:
327 log.error(_("Can't save parameters to file: %s") % e) 293 log.error(_("Can't save parameters to file: %s") % e)
328 return False 294 return False
295
296 def load(self):
297 """Load parameters and all memory things from db"""
298 #parameters data
299 return self.params.loadGenParams()
300
301 def loadIndividualParams(self, profile):
302 """Load individual parameters for a profile
303 @param profile: %(doc_profile)s"""
304 return self.params.loadIndParams(profile)
305
306 ## Profiles/Sessions management ##
307
308 def startProfileSession(self, profile):
309 """"Iniatialise session for a profile
310 @param profile: %(doc_profile)s"""
311 log.info(_("[%s] Profile session started" % profile))
312 self._entities_cache[profile] = {}
313
314 def newAuthSession(self, key, profile):
315 """Start a new session for the authenticated profile.
316
317 The personal key is loaded encrypted from a PersistentDict before being decrypted.
318
319 @param key: the key to decrypt the personal key
320 @param profile: %(doc_profile)s
321 @return: a deferred None value
322 """
323 def gotPersonalKey(personal_key):
324 """Create the session for this profile and store the personal key"""
325 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=profile)
326 log.debug('auth session created for profile %s' % profile)
327
328 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
329 d.addCallback(lambda data: BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY]))
330 return d.addCallback(gotPersonalKey)
331
332 def purgeProfileSession(self, profile):
333 """Delete cache of data of profile
334 @param profile: %(doc_profile)s"""
335 log.info(_("[%s] Profile session purge" % profile))
336 self.params.purgeProfile(profile)
337 try:
338 del self._entities_cache[profile]
339 except KeyError:
340 log.error(_("Trying to purge roster status cache for a profile not in memory: [%s]") % profile)
329 341
330 def getProfilesList(self): 342 def getProfilesList(self):
331 return self.storage.getProfilesList() 343 return self.storage.getProfilesList()
332 344
333 def getProfileName(self, profile_key, return_profile_keys=False): 345 def getProfileName(self, profile_key, return_profile_keys=False):
362 To be used for direct calls only (not through the bridge). 374 To be used for direct calls only (not through the bridge).
363 @return: a Deferred instance 375 @return: a Deferred instance
364 """ 376 """
365 self.auth_sessions.profileDelUnique(name) 377 self.auth_sessions.profileDelUnique(name)
366 return self.params.asyncDeleteProfile(name, force) 378 return self.params.asyncDeleteProfile(name, force)
379
380 ## History ##
367 381
368 def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile=C.PROF_KEY_NONE): 382 def addToHistory(self, from_jid, to_jid, message, type_='chat', extra=None, timestamp=None, profile=C.PROF_KEY_NONE):
369 assert profile != C.PROF_KEY_NONE 383 assert profile != C.PROF_KEY_NONE
370 if extra is None: 384 if extra is None:
371 extra = {} 385 extra = {}
391 limit = None 405 limit = None
392 if limit == 0: 406 if limit == 0:
393 return defer.succeed([]) 407 return defer.succeed([])
394 return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile) 408 return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile)
395 409
396 def _getLastResource(self, jid_s, profile_key): 410 ## Statuses ##
397 jid_ = jid.JID(jid_s)
398 return self.getLastResource(jid_, profile_key) or ""
399
400 def getLastResource(self, entity_jid, profile_key):
401 """Return the last resource used by an entity
402 @param entity_jid: entity jid
403 @param profile_key: %(doc_profile_key)s"""
404 data = self.getEntityData(entity_jid.userhostJID(), [C.ENTITY_LAST_RESOURCE], profile_key)
405 try:
406 return data[C.ENTITY_LAST_RESOURCE]
407 except KeyError:
408 return None
409 411
410 def _getPresenceStatuses(self, profile_key): 412 def _getPresenceStatuses(self, profile_key):
411 ret = self.getPresenceStatuses(profile_key) 413 ret = self.getPresenceStatuses(profile_key)
412 return {entity.full():data for entity, data in ret.iteritems()} 414 return {entity.full():data for entity, data in ret.iteritems()}
413 415
414 def getPresenceStatuses(self, profile_key): 416 def getPresenceStatuses(self, profile_key):
415 """Get all the presence status of a profile 417 """Get all the presence statuses of a profile
418
416 @param profile_key: %(doc_profile_key)s 419 @param profile_key: %(doc_profile_key)s
417 @return: presence data: key=entity JID, value=presence data for this entity 420 @return: presence data: key=entity JID, value=presence data for this entity
421 """
422 profile_cache = self._getProfileCache(profile_key)
423 entities_presence = {}
424
425 for entity_jid, entity_data in profile_cache.iteritems():
426 for resource, resource_data in entity_data.iteritems():
427 full_jid = copy.copy(entity_jid)
428 full_jid.resource = resource
429 try:
430 presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
431 except KeyError:
432 continue
433 entities_presence.setdefault(entity_jid, {})[resource or ''] = presence_data
434
435 return entities_presence
436
437 def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
438 """Change the presence status of an entity
439
440 @param entity_jid: jid.JID of the entity
441 @param show: show status
442 @param priority: priority
443 @param statuses: dictionary of statuses
444 @param profile_key: %(doc_profile_key)s
445 """
446 presence_data = PresenceTuple(show, priority, statuses)
447 self.updateEntityData(entity_jid, "presence", presence_data, profile_key)
448 if entity_jid.resource and show != C.PRESENCE_UNAVAILABLE:
449 # If a resource is available, bare jid should not have presence information
450 try:
451 self.delEntityDatum(entity_jid.userhostJID(), "presence", profile_key)
452 except (KeyError, exceptions.UnknownEntityError):
453 pass
454
455 ## Resources ##
456
457 def _getAllResource(self, jid_s, profile_key):
458 jid_ = jid.JID(jid_s)
459 return self.getAllResources(jid_, profile_key)
460
461 def getAllResources(self, entity_jid, profile_key):
462 """Return all resource from jid for which we have had data in this session
463
464 @param entity_jid: bare jid of the entit
465 @param profile_key: %(doc_profile_key)s
466 return (list[unicode]): list of resources
467
468 @raise exceptions.UnknownEntityError: if entity is not in cache
469 """
470 if entity_jid.resource:
471 raise ValueError("getAllResources must be used with a bare jid (got {})".format(entity_jid))
472 profile_cache = self._getProfileCache(profile_key)
473 try:
474 entity_data = profile_cache[entity_jid.userhostJID()]
475 except KeyError:
476 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
477 resources= set(entity_data.keys())
478 resources.discard(None)
479 return resources
480
481 def getAvailableResources(self, entity_jid, profile_key):
482 """Return available resource for entity_jid
483
484 This method differs from getAllResources by returning only available resources
485 @param entity_jid: bare jid of the entit
486 @param profile_key: %(doc_profile_key)s
487 return (list[unicode]): list of available resources
488
489 @raise exceptions.UnknownEntityError: if entity is not in cache
490 """
491 available = []
492 for resource in self.getAllResources(entity_jid, profile_key):
493 full_jid = copy.copy(entity_jid)
494 full_jid.resource = resource
495 try:
496 presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
497 except KeyError:
498 log.debug("Can't get presence data for {}".format(full_jid))
499 else:
500 if presence_data.show != C.PRESENCE_UNAVAILABLE:
501 available.append(resource)
502 return available
503
504 def _getMainResource(self, jid_s, profile_key):
505 jid_ = jid.JID(jid_s)
506 return self.getMainResource(jid_, profile_key) or ""
507
508 def getMainResource(self, entity_jid, profile_key):
509 """Return the main resource used by an entity
510
511 @param entity_jid: bare entity jid
512 @param profile_key: %(doc_profile_key)s
513 @return (unicode): main resource or None
514 """
515 if entity_jid.resource:
516 raise ValueError("getMainResource must be used with a bare jid (got {})".format(entity_jid))
517 resources = self.getAllResources(entity_jid, profile_key)
518 priority_resources = []
519 for resource in resources:
520 full_jid = copy.copy(entity_jid)
521 full_jid.resource = resource
522 try:
523 presence_data = self.getEntityDatum(full_jid, "presence", profile_key)
524 except KeyError:
525 log.debug("No presence information for {}".format(full_jid))
526 continue
527 priority_resources.append((resource, presence_data.priority))
528 try:
529 return max(priority_resources, key=lambda res_tuple: res_tuple[1])[0]
530 except ValueError:
531 log.warning("No resource found at all for {}".format(entity_jid))
532 return None
533
534 ## Entities data ##
535
536 def _getProfileCache(self, profile_key):
537 """Check profile validity and return its cache
538
539 @param profile: %(doc_profile_key)_s
540 @return (dict): profile cache
541
542 @raise exceptions.ProfileUnknownError: if profile doesn't exist
543 @raise exceptions.ProfileNotInCacheError: if there is no cache for this profile
418 """ 544 """
419 profile = self.getProfileName(profile_key) 545 profile = self.getProfileName(profile_key)
420 if not profile: 546 if not profile:
421 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) 547 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
422 entities_presence = {} 548 try:
423 for entity in self._entities_cache[profile]: 549 profile_cache = self._entities_cache[profile]
424 if "presence" in self._entities_cache[profile][entity]:
425 entities_presence[entity] = self._entities_cache[profile][entity]["presence"]
426
427 log.debug("Memory getPresenceStatus (%s)" % entities_presence)
428 return entities_presence
429
430 def isContactConnected(self, entity_jid, profile_key):
431 """Tell from the presence information if the given contact is connected.
432
433 @param entity_jid (JID): the entity to check
434 @param profile_key: %(doc_profile_key)s
435 @return: boolean
436 """
437 profile = self.getProfileName(profile_key)
438 if not profile:
439 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
440 try:
441 presence = self._entities_cache[profile][entity_jid]['presence']
442 return len([True for status in presence.values() if status[0] != 'unavailable']) > 0
443 except KeyError: 550 except KeyError:
444 return False
445
446 def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
447 """Change the presence status of an entity
448 @param entity_jid: jid.JID of the entity
449 @param show: show status
450 @param priotity: priotity
451 @param statuses: dictionary of statuses
452 @param profile_key: %(doc_profile_key)s
453 """
454 profile = self.getProfileName(profile_key)
455 if not profile:
456 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
457 entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
458 resource = entity_jid.resource
459 if resource:
460 try:
461 type_ = self.getEntityDatum(entity_jid.userhostJID(), 'type', profile)
462 except KeyError:
463 type_ = 'contact'
464 if type_ != 'chatroom':
465 self.updateEntityData(entity_jid.userhostJID(), C.ENTITY_LAST_RESOURCE, resource, profile)
466 entity_data.setdefault("presence", {})[resource or ''] = (show, priority, statuses)
467
468 def _getEntitiesData(self, entity_jid, profile):
469 """Get data dictionary for entities
470 @param entity_jid: JID of the entity, or C.ENTITY_ALL for all entities)
471 @param profile: %(doc_profile)s
472 @return: entities_data (key=jid, value=entity_data)
473 @raise: exceptions.ProfileNotInCacheError if profile is not in cache
474 """
475 if not profile in self._entities_cache:
476 raise exceptions.ProfileNotInCacheError 551 raise exceptions.ProfileNotInCacheError
477 if entity_jid == C.ENTITY_ALL: 552 return profile_cache
478 entities_data = self._entities_cache[profile] 553
554 def setSignalOnUpdate(self, key, signal=True):
555 """Set a signal flag on the key
556
557 When the key will be updated, a signal will be sent to frontends
558 @param key: key to signal
559 @param signal(boolean): if True, do the signal
560 """
561 if signal:
562 self._key_signals.add(key)
479 else: 563 else:
480 entity_data = self._entities_cache[profile].setdefault(entity_jid, {}) 564 self._key_signals.discard(key)
481 entities_data = {entity_jid: entity_data} 565
482 return entities_data 566 def getAllEntitiesIter(self, with_bare=False, profile_key=C.PROF_KEY_NONE):
483 567 """Return an iterator of full jids of all entities in cache
484 def _updateEntityResources(self, entity_jid, profile): 568
485 """Add a known resource to bare entity_jid cache 569 @param with_bare: if True, include bare jids
486 @param entity_jid: full entity_jid (with resource) 570 @param profile_key: %(doc_profile_key)s
487 @param profile: %(doc_profile)s 571 @return (list[unicode]): list of jids
488 """ 572 """
489 assert(entity_jid.resource) 573 profile_cache = self._getProfileCache(profile_key)
490 entity_data = self._getEntitiesData(entity_jid.userhostJID(), profile)[entity_jid.userhostJID()] 574 # we construct a list of all known full jids (bare jid of entities x resources)
491 resources = entity_data.setdefault('resources', set()) 575 for bare_jid, entity_data in profile_cache.iteritems():
492 resources.add(entity_jid.resource) 576 for resource in entity_data.iterkeys():
577 if resource is None:
578 continue
579 full_jid = copy.copy(bare_jid)
580 full_jid.resource = resource
581 yield full_jid
493 582
494 def updateEntityData(self, entity_jid, key, value, profile_key): 583 def updateEntityData(self, entity_jid, key, value, profile_key):
495 """Set a misc data for an entity 584 """Set a misc data for an entity
496 @param entity_jid: JID of the entity, or C.ENTITY_ALL to update all entities) 585
586 If key was registered with setSignalOnUpdate, a signal will be sent to frontends
587 @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
588 C.ENTITY_ALL for all entities (all resources + bare jids)
497 @param key: key to set (eg: "type") 589 @param key: key to set (eg: "type")
498 @param value: value for this key (eg: "chatroom") 590 @param value: value for this key (eg: "chatroom")
499 @param profile_key: %(doc_profile_key)s 591 @param profile_key: %(doc_profile_key)s
500 """ 592 """
501 profile = self.getProfileName(profile_key) 593 profile_cache = self._getProfileCache(profile_key)
502 if not profile: 594 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
503 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) 595 entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
504 entities_data = self._getEntitiesData(entity_jid, profile) 596 else:
505 if entity_jid != C.ENTITY_ALL and entity_jid.resource: 597 entities = (entity_jid,)
506 self._updateEntityResources(entity_jid, profile) 598
507 599 for jid_ in entities:
508 for jid_ in entities_data: 600 entity_data = profile_cache.setdefault(jid_.userhostJID(),{}).setdefault(jid_.resource, {})
509 entity_data = entities_data[jid_] 601
510 if value == C.PROF_KEY_NONE and key in entity_data: 602 entity_data[key] = value
511 del entity_data[key] 603 if key in self._key_signals:
512 else: 604 if not isinstance(value, basestring):
513 entity_data[key] = value 605 log.error(u"Setting a non string value ({}) for a key ({}) which has a signal flag".format(value, key))
514 if isinstance(value, basestring): 606 else:
515 self.host.bridge.entityDataUpdated(jid_.full(), key, value, profile) 607 self.host.bridge.entityDataUpdated(jid_.full(), key, value, self.getProfileName(profile_key))
516 608
517 def delEntityData(self, entity_jid, key, profile_key): 609 def delEntityDatum(self, entity_jid, key, profile_key):
518 """Delete data for an entity 610 """Delete a data for an entity
519 @param entity_jid: JID of the entity, or C.ENTITY_ALL to delete data from all entities) 611
612 @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
613 C.ENTITY_ALL for all entities (all resources + bare jids)
520 @param key: key to delete (eg: "type") 614 @param key: key to delete (eg: "type")
521 @param profile_key: %(doc_profile_key)s 615 @param profile_key: %(doc_profile_key)s
522 """ 616
523 entities_data = self._getEntitiesData(entity_jid, profile_key) 617 @raise exceptions.UnknownEntityError: if entity is not in cache
524 for entity_jid in entities_data: 618 @raise KeyError: key is not in cache
525 entity_data = entities_data[entity_jid] 619 """
620 profile_cache = self._getProfileCache(profile_key)
621 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
622 entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
623 else:
624 entities = (entity_jid,)
625
626 for jid_ in entities:
627 try:
628 entity_data = profile_cache[jid_.userhostJID()][jid_.resource]
629 except KeyError:
630 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(jid_))
526 try: 631 try:
527 del entity_data[key] 632 del entity_data[key]
528 except KeyError: 633 except KeyError as e:
529 log.debug("Key [%s] doesn't exist for [%s] in entities_cache" % (key, entity_jid.full())) 634 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
530 635 continue # we ignore KeyError when deleting keys from several entities
531 def getEntityData(self, entity_jid, keys_list, profile_key): 636 else:
637 raise e
638
639 def getEntityData(self, entity_jid, keys_list=None, profile_key=C.PROF_KEY_NONE):
532 """Get a list of cached values for entity 640 """Get a list of cached values for entity
533 641
534 @param entity_jid: JID of the entity 642 @param entity_jid: JID of the entity
535 @param keys_list (iterable): list of keys to get, empty list for everything 643 @param keys_list (iterable,None): list of keys to get, None for everything
536 @param profile_key: %(doc_profile_key)s 644 @param profile_key: %(doc_profile_key)s
537 @return: dict withs values for each key in keys_list. 645 @return: dict withs values for each key in keys_list.
538 if there is no value of a given key, resulting dict will 646 if there is no value of a given key, resulting dict will
539 have nothing with that key nether 647 have nothing with that key nether
540 @raise: exceptions.UnknownEntityError if entity is not in cache 648
541 """ 649 @raise exceptions.UnknownEntityError: if entity is not in cache
542 profile = self.getProfileName(profile_key) 650 """
543 if not profile: 651 profile_cache = self._getProfileCache(profile_key)
544 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) 652 try:
545 entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid] 653 entity_data = profile_cache[entity_jid.userhostJID()][entity_jid.resource]
546 if not keys_list: 654 except KeyError:
655 raise exceptions.UnknownEntityError(u"Entity {} not in cache (was requesting {})".format(entity_jid, keys_list))
656 if keys_list is None:
547 return entity_data 657 return entity_data
548 ret = {} 658
549 for key in keys_list: 659 return {key: entity_data[key] for key in keys_list if key in entity_data}
550 if key in entity_data:
551 ret[key] = entity_data[key]
552 return ret
553 660
554 def getEntityDatum(self, entity_jid, key, profile_key): 661 def getEntityDatum(self, entity_jid, key, profile_key):
555 """Get a datum from entity 662 """Get a datum from entity
556 663
557 @param entity_jid: JID of the entity 664 @param entity_jid: JID of the entity
558 @param keys: key to get 665 @param keys: key to get
559 @param profile_key: %(doc_profile_key)s 666 @param profile_key: %(doc_profile_key)s
560 @return: requested value 667 @return: requested value
561 668
562 @raise: exceptions.UnknownEntityError if entity is not in cache 669 @raise exceptions.UnknownEntityError: if entity is not in cache
563 @raise: KeyError if there is no value for this key and this entity 670 @raise KeyError: if there is no value for this key and this entity
564 """ 671 """
565 return self.getEntityData(entity_jid, (key,), profile_key)[key] 672 return self.getEntityData(entity_jid, (key,), profile_key)[key]
566 673
567 def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE): 674 def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE):
568 """Remove cached data for entity 675 """Remove all cached data for entity
676
569 @param entity_jid: JID of the entity to delete 677 @param entity_jid: JID of the entity to delete
570 @param delete_all_resources: if True also delete all known resources form cache 678 @param delete_all_resources: if True also delete all known resources from cache (a bare jid must be given in this case)
571 @param profile_key: %(doc_profile_key)s 679 @param profile_key: %(doc_profile_key)s
572 """ 680
573 profile = self.getProfileName(profile_key) 681 @raise exceptions.UnknownEntityError: if entity is not in cache
574 if not profile: 682 """
575 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) 683 profile_cache = self._getProfileCache(profile_key)
576 to_delete = set([entity_jid])
577 684
578 if delete_all_resources: 685 if delete_all_resources:
579 if entity_jid.resource: 686 if entity_jid.resource:
580 raise ValueError(_("Need a bare jid to delete all resources")) 687 raise ValueError(_("Need a bare jid to delete all resources"))
581 entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
582 resources = entity_data.setdefault('resources', set())
583 to_delete.update([jid.JID("%s/%s" % (entity_jid.userhost(), resource)) for resource in resources])
584
585 for entity in to_delete:
586 try: 688 try:
587 del self._entities_cache[profile][entity] 689 del profile_cache[entity_jid]
588 except KeyError: 690 except KeyError:
589 log.warning(_("Can't delete entity [{}]: not in cache").format(entity.full())) 691 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
692 else:
693 try:
694 del profile_cache[entity_jid.userhostJID()][entity_jid.resource]
695 except KeyError:
696 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
697
698 ## Encryption ##
590 699
591 def encryptValue(self, value, profile): 700 def encryptValue(self, value, profile):
592 """Encrypt a value for the given profile. The personal key must be loaded 701 """Encrypt a value for the given profile. The personal key must be loaded
593 already in the profile session, that should be the case if the profile is 702 already in the profile session, that should be the case if the profile is
594 already authenticated. 703 already authenticated.
641 log.debug(_('Personal data (%(ns)s, %(key)s) has been successfuly encrypted') % 750 log.debug(_('Personal data (%(ns)s, %(key)s) has been successfuly encrypted') %
642 {'ns': C.MEMORY_CRYPTO_NAMESPACE, 'key': data_key}) 751 {'ns': C.MEMORY_CRYPTO_NAMESPACE, 'key': data_key})
643 752
644 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load() 753 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
645 return d.addCallback(gotIndMemory).addCallback(done) 754 return d.addCallback(gotIndMemory).addCallback(done)
755
756 ## Subscription requests ##
646 757
647 def addWaitingSub(self, type_, entity_jid, profile_key): 758 def addWaitingSub(self, type_, entity_jid, profile_key):
648 """Called when a subcription request is received""" 759 """Called when a subcription request is received"""
649 profile = self.getProfileName(profile_key) 760 profile = self.getProfileName(profile_key)
650 assert profile 761 assert profile
668 if profile not in self.subscriptions: 779 if profile not in self.subscriptions:
669 return {} 780 return {}
670 781
671 return self.subscriptions[profile] 782 return self.subscriptions[profile]
672 783
784 ## Parameters ##
785
673 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): 786 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
674 return self.params.getStringParamA(name, category, attr, profile_key) 787 return self.params.getStringParamA(name, category, attr, profile_key)
675 788
676 def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): 789 def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
677 return self.params.getParamA(name, category, attr, profile_key) 790 return self.params.getParamA(name, category, attr, profile_key)
703 def paramsRegisterApp(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=''): 816 def paramsRegisterApp(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=''):
704 return self.params.paramsRegisterApp(xml, security_limit, app) 817 return self.params.paramsRegisterApp(xml, security_limit, app)
705 818
706 def setDefault(self, name, category, callback, errback=None): 819 def setDefault(self, name, category, callback, errback=None):
707 return self.params.setDefault(name, category, callback, errback) 820 return self.params.setDefault(name, category, callback, errback)
821
822 ## Misc ##
823
824 def isEntityAvailable(self, entity_jid, profile_key):
825 """Tell from the presence information if the given entity is available.
826
827 @param entity_jid (JID): the entity to check (if bare jid is used, all resources are tested)
828 @param profile_key: %(doc_profile_key)s
829 @return (bool): True if entity is available
830 """
831 if not entity_jid.resource:
832 return bool(self.getAvailableResources) # is any resource is available, entity is available
833 try:
834 presence_data = self.getEntityDatum(entity_jid, "presence", profile_key)
835 except KeyError:
836 log.debug("No presence information for {}".format(entity_jid))
837 return False
838 return presence_data.show != C.PRESENCE_UNAVAILABLE