comparison 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
comparison
equal deleted inserted replaced
1295:1e3b1f9ad6e2 1367:f71a0fc26886
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
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
329 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)
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):
334 """Return name of profile from keyword 346 """Return name of profile from keyword
347
335 @param profile_key: can be the profile name or a keywork (like @DEFAULT@) 348 @param profile_key: can be the profile name or a keywork (like @DEFAULT@)
336 @return: profile name or None if it doesn't exist""" 349 @param return_profile_keys: if True, return unmanaged profile keys (like "@ALL@"). This keys must be managed by the caller
350 @return: requested profile name or emptry string if it doesn't exist
351 """
337 return self.params.getProfileName(profile_key, return_profile_keys) 352 return self.params.getProfileName(profile_key, return_profile_keys)
338 353
339 def asyncCreateProfile(self, name, password=''): 354 def asyncCreateProfile(self, name, password=''):
340 """Create a new profile 355 """Create a new profile
341 @param name: profile name 356 @param name: profile name
342 @param password: profile password 357 @param password: profile password
343 @return: Deferred 358 @return: Deferred
344 """ 359 """
360 if not name:
361 raise ValueError("Empty profile name")
362 if name[0] == '@':
363 raise ValueError("A profile name can't start with a '@'")
345 personal_key = BlockCipher.getRandomKey(base64=True) # generated once for all and saved in a PersistentDict 364 personal_key = BlockCipher.getRandomKey(base64=True) # generated once for all and saved in a PersistentDict
346 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=name) # will be encrypted by setParam 365 self.auth_sessions.newSession({C.MEMORY_CRYPTO_KEY: personal_key}, profile=name) # will be encrypted by setParam
347 d = self.params.asyncCreateProfile(name) 366 d = self.params.asyncCreateProfile(name)
348 d.addCallback(lambda dummy: self.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=name)) 367 d.addCallback(lambda dummy: self.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=name))
349 return d 368 return d
355 To be used for direct calls only (not through the bridge). 374 To be used for direct calls only (not through the bridge).
356 @return: a Deferred instance 375 @return: a Deferred instance
357 """ 376 """
358 self.auth_sessions.profileDelUnique(name) 377 self.auth_sessions.profileDelUnique(name)
359 return self.params.asyncDeleteProfile(name, force) 378 return self.params.asyncDeleteProfile(name, force)
379
380 ## History ##
360 381
361 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):
362 assert profile != C.PROF_KEY_NONE 383 assert profile != C.PROF_KEY_NONE
363 if extra is None: 384 if extra is None:
364 extra = {} 385 extra = {}
384 limit = None 405 limit = None
385 if limit == 0: 406 if limit == 0:
386 return defer.succeed([]) 407 return defer.succeed([])
387 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)
388 409
389 def _getLastResource(self, jid_s, profile_key): 410 ## Statuses ##
390 jid_ = jid.JID(jid_s)
391 return self.getLastResource(jid_, profile_key) or ""
392
393 def getLastResource(self, entity_jid, profile_key):
394 """Return the last resource used by an entity
395 @param entity_jid: entity jid
396 @param profile_key: %(doc_profile_key)s"""
397 data = self.getEntityData(entity_jid.userhostJID(), [C.ENTITY_LAST_RESOURCE], profile_key)
398 try:
399 return data[C.ENTITY_LAST_RESOURCE]
400 except KeyError:
401 return None
402 411
403 def _getPresenceStatuses(self, profile_key): 412 def _getPresenceStatuses(self, profile_key):
404 ret = self.getPresenceStatuses(profile_key) 413 ret = self.getPresenceStatuses(profile_key)
405 return {entity.full():data for entity, data in ret.iteritems()} 414 return {entity.full():data for entity, data in ret.iteritems()}
406 415
407 def getPresenceStatuses(self, profile_key): 416 def getPresenceStatuses(self, profile_key):
408 """Get all the presence status of a profile 417 """Get all the presence statuses of a profile
418
409 @param profile_key: %(doc_profile_key)s 419 @param profile_key: %(doc_profile_key)s
410 @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=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
411 """ 544 """
412 profile = self.getProfileName(profile_key) 545 profile = self.getProfileName(profile_key)
413 if not profile: 546 if not profile:
414 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'))
415 entities_presence = {} 548 try:
416 for entity in self._entities_cache[profile]: 549 profile_cache = self._entities_cache[profile]
417 if "presence" in self._entities_cache[profile][entity]:
418 entities_presence[entity] = self._entities_cache[profile][entity]["presence"]
419
420 log.debug("Memory getPresenceStatus (%s)" % entities_presence)
421 return entities_presence
422
423 def isContactConnected(self, entity_jid, profile_key):
424 """Tell from the presence information if the given contact is connected.
425
426 @param entity_jid (JID): the entity to check
427 @param profile_key: %(doc_profile_key)s
428 @return: boolean
429 """
430 profile = self.getProfileName(profile_key)
431 if not profile:
432 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
433 try:
434 presence = self._entities_cache[profile][entity_jid]['presence']
435 return len([True for status in presence.values() if status[0] != 'unavailable']) > 0
436 except KeyError: 550 except KeyError:
437 return False
438
439 def setPresenceStatus(self, entity_jid, show, priority, statuses, profile_key):
440 """Change the presence status of an entity
441 @param entity_jid: jid.JID of the entity
442 @param show: show status
443 @param priotity: priotity
444 @param statuses: dictionary of statuses
445 @param profile_key: %(doc_profile_key)s
446 """
447 profile = self.getProfileName(profile_key)
448 if not profile:
449 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile'))
450 entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
451 resource = entity_jid.resource
452 if resource:
453 try:
454 type_ = self.getEntityDatum(entity_jid.userhostJID(), 'type', profile)
455 except KeyError:
456 type_ = 'contact'
457 if type_ != 'chatroom':
458 self.updateEntityData(entity_jid.userhostJID(), C.ENTITY_LAST_RESOURCE, resource, profile)
459 entity_data.setdefault("presence", {})[resource or ''] = (show, priority, statuses)
460
461 def _getEntitiesData(self, entity_jid, profile):
462 """Get data dictionary for entities
463 @param entity_jid: JID of the entity, or C.ENTITY_ALL for all entities)
464 @param profile: %(doc_profile)s
465 @return: entities_data (key=jid, value=entity_data)
466 @raise: exceptions.ProfileNotInCacheError if profile is not in cache
467 """
468 if not profile in self._entities_cache:
469 raise exceptions.ProfileNotInCacheError 551 raise exceptions.ProfileNotInCacheError
470 if entity_jid == C.ENTITY_ALL: 552 return profile_cache
471 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)
472 else: 563 else:
473 entity_data = self._entities_cache[profile].setdefault(entity_jid, {}) 564 self._key_signals.discard(key)
474 entities_data = {entity_jid: entity_data} 565
475 return entities_data 566 def getAllEntitiesIter(self, with_bare=False, profile_key=C.PROF_KEY_NONE):
476 567 """Return an iterator of full jids of all entities in cache
477 def _updateEntityResources(self, entity_jid, profile): 568
478 """Add a known resource to bare entity_jid cache 569 @param with_bare: if True, include bare jids
479 @param entity_jid: full entity_jid (with resource) 570 @param profile_key: %(doc_profile_key)s
480 @param profile: %(doc_profile)s 571 @return (list[unicode]): list of jids
481 """ 572 """
482 assert(entity_jid.resource) 573 profile_cache = self._getProfileCache(profile_key)
483 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)
484 resources = entity_data.setdefault('resources', set()) 575 for bare_jid, entity_data in profile_cache.iteritems():
485 resources.add(entity_jid.resource) 576 for resource in entity_data.iterkeys():
486 577 if resource is None:
487 def updateEntityData(self, entity_jid, key, value, profile_key): 578 continue
579 full_jid = copy.copy(bare_jid)
580 full_jid.resource = resource
581 yield full_jid
582
583 def updateEntityData(self, entity_jid, key, value, silent=False, profile_key=C.PROF_KEY_NONE):
488 """Set a misc data for an entity 584 """Set a misc data for an entity
489 @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)
490 @param key: key to set (eg: "type") 589 @param key: key to set (eg: "type")
491 @param value: value for this key (eg: "chatroom") 590 @param value: value for this key (eg: "chatroom")
492 @param profile_key: %(doc_profile_key)s 591 @param silent(bool): if True, doesn't send signal to frontend, even there is a signal flag (see setSignalOnUpdate)
493 """ 592 @param profile_key: %(doc_profile_key)s
494 profile = self.getProfileName(profile_key) 593 """
495 if not profile: 594 profile_cache = self._getProfileCache(profile_key)
496 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) 595 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
497 entities_data = self._getEntitiesData(entity_jid, profile) 596 entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
498 if entity_jid != C.ENTITY_ALL and entity_jid.resource: 597 else:
499 self._updateEntityResources(entity_jid, profile) 598 entities = (entity_jid,)
500 599
501 for jid_ in entities_data: 600 for jid_ in entities:
502 entity_data = entities_data[jid_] 601 entity_data = profile_cache.setdefault(jid_.userhostJID(),{}).setdefault(jid_.resource, {})
503 if value == C.PROF_KEY_NONE and key in entity_data: 602
504 del entity_data[key] 603 entity_data[key] = value
505 else: 604 if key in self._key_signals and not silent:
506 entity_data[key] = value 605 if not isinstance(value, basestring):
507 if isinstance(value, basestring): 606 log.error(u"Setting a non string value ({}) for a key ({}) which has a signal flag".format(value, key))
508 self.host.bridge.entityDataUpdated(jid_.full(), key, value, profile) 607 else:
509 608 self.host.bridge.entityDataUpdated(jid_.full(), key, value, self.getProfileName(profile_key))
510 def delEntityData(self, entity_jid, key, profile_key): 609
511 """Delete data for an entity 610 def delEntityDatum(self, entity_jid, key, profile_key):
512 @param entity_jid: JID of the entity, or C.ENTITY_ALL to delete data from all entities) 611 """Delete a data for an entity
612
613 @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities,
614 C.ENTITY_ALL for all entities (all resources + bare jids)
513 @param key: key to delete (eg: "type") 615 @param key: key to delete (eg: "type")
514 @param profile_key: %(doc_profile_key)s 616 @param profile_key: %(doc_profile_key)s
515 """ 617
516 entities_data = self._getEntitiesData(entity_jid, profile_key) 618 @raise exceptions.UnknownEntityError: if entity is not in cache
517 for entity_jid in entities_data: 619 @raise KeyError: key is not in cache
518 entity_data = entities_data[entity_jid] 620 """
621 profile_cache = self._getProfileCache(profile_key)
622 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
623 entities = self.getAllEntitiesIter(entity_jid==C.ENTITY_ALL, profile_key)
624 else:
625 entities = (entity_jid,)
626
627 for jid_ in entities:
628 try:
629 entity_data = profile_cache[jid_.userhostJID()][jid_.resource]
630 except KeyError:
631 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(jid_))
519 try: 632 try:
520 del entity_data[key] 633 del entity_data[key]
521 except KeyError: 634 except KeyError as e:
522 log.debug("Key [%s] doesn't exist for [%s] in entities_cache" % (key, entity_jid.full())) 635 if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL):
523 636 continue # we ignore KeyError when deleting keys from several entities
524 def getEntityData(self, entity_jid, keys_list, profile_key): 637 else:
525 """Get a list of cached values for entity 638 raise e
526 639
527 @param entity_jid: JID of the entity 640 def _getEntitiesData(self, entities_jids, keys_list, profile_key):
528 @param keys_list (iterable): list of keys to get, empty list for everything 641 ret = self.getEntitiesData([jid.JID(jid_) for jid_ in entities_jids], keys_list, profile_key)
642 return {jid_.full(): data for jid_, data in ret.iteritems()}
643
644 def getEntitiesData(self, entities_jids, keys_list=None, profile_key=C.PROF_KEY_NONE):
645 """Get a list of cached values for several entities at once
646
647 @param entities_jids: jids of the entities, or empty list for all entities in cache
648 @param keys_list (iterable,None): list of keys to get, None for everything
529 @param profile_key: %(doc_profile_key)s 649 @param profile_key: %(doc_profile_key)s
530 @return: dict withs values for each key in keys_list. 650 @return: dict withs values for each key in keys_list.
531 if there is no value of a given key, resulting dict will 651 if there is no value of a given key, resulting dict will
532 have nothing with that key nether 652 have nothing with that key nether
533 @raise: exceptions.UnknownEntityError if entity is not in cache 653 if an entity doesn't exist in cache, it will not appear
534 """ 654 in resulting dict
535 profile = self.getProfileName(profile_key) 655
536 if not profile: 656 @raise exceptions.UnknownEntityError: if entity is not in cache
537 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) 657 """
538 entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid] 658 def fillEntityData(entity_cache_data):
539 if not keys_list: 659 entity_data = {}
660 if keys_list is None:
661 entity_data = entity_cache_data
662 else:
663 for key in keys_list:
664 try:
665 entity_data[key] = entity_cache_data[key]
666 except KeyError:
667 continue
540 return entity_data 668 return entity_data
541 ret = {} 669
542 for key in keys_list: 670 profile_cache = self._getProfileCache(profile_key)
543 if key in entity_data: 671 ret_data = {}
544 ret[key] = entity_data[key] 672 if entities_jids:
545 return ret 673 for entity in entities_jids:
674 try:
675 entity_cache_data = profile_cache[entity.userhostJID()][entity.resource]
676 except KeyError:
677 continue
678 ret_data[entity.full()] = fillEntityData(entity_cache_data, keys_list)
679 else:
680 for bare_jid, data in profile_cache.iteritems():
681 for resource, entity_cache_data in data.iteritems():
682 full_jid = copy.copy(bare_jid)
683 full_jid.resource = resource
684 ret_data[full_jid] = fillEntityData(entity_cache_data)
685
686 return ret_data
687
688 def getEntityData(self, entity_jid, keys_list=None, profile_key=C.PROF_KEY_NONE):
689 """Get a list of cached values for entity
690
691 @param entity_jid: JID of the entity
692 @param keys_list (iterable,None): list of keys to get, None for everything
693 @param profile_key: %(doc_profile_key)s
694 @return: dict withs values for each key in keys_list.
695 if there is no value of a given key, resulting dict will
696 have nothing with that key nether
697
698 @raise exceptions.UnknownEntityError: if entity is not in cache
699 """
700 profile_cache = self._getProfileCache(profile_key)
701 try:
702 entity_data = profile_cache[entity_jid.userhostJID()][entity_jid.resource]
703 except KeyError:
704 raise exceptions.UnknownEntityError(u"Entity {} not in cache (was requesting {})".format(entity_jid, keys_list))
705 if keys_list is None:
706 return entity_data
707
708 return {key: entity_data[key] for key in keys_list if key in entity_data}
546 709
547 def getEntityDatum(self, entity_jid, key, profile_key): 710 def getEntityDatum(self, entity_jid, key, profile_key):
548 """Get a datum from entity 711 """Get a datum from entity
549 712
550 @param entity_jid: JID of the entity 713 @param entity_jid: JID of the entity
551 @param keys: key to get 714 @param keys: key to get
552 @param profile_key: %(doc_profile_key)s 715 @param profile_key: %(doc_profile_key)s
553 @return: requested value 716 @return: requested value
554 717
555 @raise: exceptions.UnknownEntityError if entity is not in cache 718 @raise exceptions.UnknownEntityError: if entity is not in cache
556 @raise: KeyError if there is no value for this key and this entity 719 @raise KeyError: if there is no value for this key and this entity
557 """ 720 """
558 return self.getEntityData(entity_jid, (key,), profile_key)[key] 721 return self.getEntityData(entity_jid, (key,), profile_key)[key]
559 722
560 def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE): 723 def delEntityCache(self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE):
561 """Remove cached data for entity 724 """Remove all cached data for entity
725
562 @param entity_jid: JID of the entity to delete 726 @param entity_jid: JID of the entity to delete
563 @param delete_all_resources: if True also delete all known resources form cache 727 @param delete_all_resources: if True also delete all known resources from cache (a bare jid must be given in this case)
564 @param profile_key: %(doc_profile_key)s 728 @param profile_key: %(doc_profile_key)s
565 """ 729
566 profile = self.getProfileName(profile_key) 730 @raise exceptions.UnknownEntityError: if entity is not in cache
567 if not profile: 731 """
568 raise exceptions.ProfileUnknownError(_('Trying to get entity data for a non-existant profile')) 732 profile_cache = self._getProfileCache(profile_key)
569 to_delete = set([entity_jid])
570 733
571 if delete_all_resources: 734 if delete_all_resources:
572 if entity_jid.resource: 735 if entity_jid.resource:
573 raise ValueError(_("Need a bare jid to delete all resources")) 736 raise ValueError(_("Need a bare jid to delete all resources"))
574 entity_data = self._getEntitiesData(entity_jid, profile)[entity_jid]
575 resources = entity_data.setdefault('resources', set())
576 to_delete.update([jid.JID("%s/%s" % (entity_jid.userhost(), resource)) for resource in resources])
577
578 for entity in to_delete:
579 try: 737 try:
580 del self._entities_cache[profile][entity] 738 del profile_cache[entity_jid]
581 except KeyError: 739 except KeyError:
582 log.debug("Can't delete entity [%s]: not in cache" % entity.full()) 740 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
741 else:
742 try:
743 del profile_cache[entity_jid.userhostJID()][entity_jid.resource]
744 except KeyError:
745 raise exceptions.UnknownEntityError(u"Entity {} not in cache".format(entity_jid))
746
747 ## Encryption ##
583 748
584 def encryptValue(self, value, profile): 749 def encryptValue(self, value, profile):
585 """Encrypt a value for the given profile. The personal key must be loaded 750 """Encrypt a value for the given profile. The personal key must be loaded
586 already in the profile session, that should be the case if the profile is 751 already in the profile session, that should be the case if the profile is
587 already authenticated. 752 already authenticated.
634 log.debug(_('Personal data (%(ns)s, %(key)s) has been successfuly encrypted') % 799 log.debug(_('Personal data (%(ns)s, %(key)s) has been successfuly encrypted') %
635 {'ns': C.MEMORY_CRYPTO_NAMESPACE, 'key': data_key}) 800 {'ns': C.MEMORY_CRYPTO_NAMESPACE, 'key': data_key})
636 801
637 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load() 802 d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load()
638 return d.addCallback(gotIndMemory).addCallback(done) 803 return d.addCallback(gotIndMemory).addCallback(done)
804
805 ## Subscription requests ##
639 806
640 def addWaitingSub(self, type_, entity_jid, profile_key): 807 def addWaitingSub(self, type_, entity_jid, profile_key):
641 """Called when a subcription request is received""" 808 """Called when a subcription request is received"""
642 profile = self.getProfileName(profile_key) 809 profile = self.getProfileName(profile_key)
643 assert profile 810 assert profile
661 if profile not in self.subscriptions: 828 if profile not in self.subscriptions:
662 return {} 829 return {}
663 830
664 return self.subscriptions[profile] 831 return self.subscriptions[profile]
665 832
833 ## Parameters ##
834
666 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): 835 def getStringParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
667 return self.params.getStringParamA(name, category, attr, profile_key) 836 return self.params.getStringParamA(name, category, attr, profile_key)
668 837
669 def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): 838 def getParamA(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE):
670 return self.params.getParamA(name, category, attr, profile_key) 839 return self.params.getParamA(name, category, attr, profile_key)
696 def paramsRegisterApp(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=''): 865 def paramsRegisterApp(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=''):
697 return self.params.paramsRegisterApp(xml, security_limit, app) 866 return self.params.paramsRegisterApp(xml, security_limit, app)
698 867
699 def setDefault(self, name, category, callback, errback=None): 868 def setDefault(self, name, category, callback, errback=None):
700 return self.params.setDefault(name, category, callback, errback) 869 return self.params.setDefault(name, category, callback, errback)
870
871 ## Misc ##
872
873 def isEntityAvailable(self, entity_jid, profile_key):
874 """Tell from the presence information if the given entity is available.
875
876 @param entity_jid (JID): the entity to check (if bare jid is used, all resources are tested)
877 @param profile_key: %(doc_profile_key)s
878 @return (bool): True if entity is available
879 """
880 if not entity_jid.resource:
881 return bool(self.getAvailableResources) # is any resource is available, entity is available
882 try:
883 presence_data = self.getEntityDatum(entity_jid, "presence", profile_key)
884 except KeyError:
885 log.debug("No presence information for {}".format(entity_jid))
886 return False
887 return presence_data.show != C.PRESENCE_UNAVAILABLE