Mercurial > libervia-backend
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 |