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
This commit is contained in:
Goffi 2015-01-24 01:00:29 +01:00
commit 8ce5c1862b
15 changed files with 1125 additions and 452 deletions

View file

@ -285,8 +285,8 @@ class DbusObject(dbus.service.Object):
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='ss', out_signature='s',
async_callbacks=None)
def getLastResource(self, contact_jid, profile_key="@DEFAULT@"):
return self._callback("getLastResource", unicode(contact_jid), unicode(profile_key))
def getMainResource(self, contact_jid, profile_key="@DEFAULT@"):
return self._callback("getMainResource", unicode(contact_jid), unicode(profile_key))
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
in_signature='ss', out_signature='s',

View file

@ -446,17 +446,35 @@ class DbusConstructor(Constructor):
'args': self.getArguments(function['sig_in'], name=arg_doc, default=default)}
if function["type"] == "method":
# XXX: we can manage blocking call in the same way as async one: if callback is None the call will be blocking
completion['debug'] = "" if not self.options.debug else 'log.debug ("%s")\n%s' % (section, 8 * ' ')
completion['args_result'] = self.getArguments(function['sig_in'], name=arg_doc)
completion['async_args'] = 'callback=None, errback=None' if async else ''
completion['async_comma'] = ', ' if async and function['sig_in'] else ''
completion['error_handler'] = ("error_handler = None if callback is None else lambda err:errback(dbus_to_bridge_exception(err))\n" + 8*" ") if async else ''
completion['async_args_result'] = 'timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler' if async else ''
completion['async_args'] = 'callback=None, errback=None'
completion['async_comma'] = ', ' if function['sig_in'] else ''
completion['error_handler'] = """if callback is None:
error_handler = None
else:
if errback is None:
errback = log.error
error_handler = lambda err:errback(dbus_to_bridge_exception(err))
"""
if async:
completion['blocking_call'] = ''
completion['async_args_result'] = 'timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler'
else:
# XXX: To have a blocking call, we must have not reply_handler, so we test if callback exists, and add reply_handler only in this case
completion['blocking_call'] = """kwargs={}
if callback is not None:
kwargs['timeout'] = const_TIMEOUT
kwargs['reply_handler'] = callback
kwargs['error_handler'] = error_handler
"""
completion['async_args_result'] = '**kwargs'
result = "self.db_%(category)s_iface.%(name)s(%(args_result)s%(async_comma)s%(async_args_result)s)" % completion
completion['result'] = ("unicode(%s)" if self.options.unicode and function['sig_out'] == 's' else "%s") % result
methods_part.append("""\
def %(name)s(self, %(args)s%(async_comma)s%(async_args)s):
%(error_handler)s%(debug)sreturn %(result)s
%(error_handler)s%(blocking_call)s%(debug)sreturn %(result)s
""" % completion)
#at this point, methods_part should be filled,

View file

@ -279,7 +279,7 @@ doc_param_0=group: name of the group to check
doc_param_1=%(doc_profile_key)s
doc_return=array of jids
[getLastResource]
[getMainResource]
type=method
category=core
sig_in=ss
@ -288,7 +288,7 @@ param_1_default="@DEFAULT@"
doc=Return the last resource connected for a contact
doc_param_0=contact_jid: jid of the contact
doc_param_1=%(doc_profile_key)s
doc_return=the last resource connected of the contact, or ""
doc_return=the resource connected of the contact with highest priority, or ""
[getPresenceStatuses]
type=method

View file

@ -102,10 +102,10 @@ class DBusBridgeFrontend(BridgeFrontend):
async = False
if kwargs:
if 'callback' in kwargs and 'errback' in kwargs:
if 'callback' in kwargs:
async = True
_callback = kwargs.pop('callback')
_errback = kwargs.pop('errback')
_errback = kwargs.pop('errback', lambda failure: log.error(unicode(failure)))
elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]):
async = True
args = list(args)
@ -122,4 +122,5 @@ class DBusBridgeFrontend(BridgeFrontend):
return method(*args, **kwargs)
return getPluginMethod
##METHODS_PART##

View file

@ -76,7 +76,8 @@ class Const(object):
PROF_KEY_NONE = '@NONE@'
PROF_KEY_DEFAULT = '@DEFAULT@'
ENTITY_ALL = '@ALL@'
ENTITY_LAST_RESOURCE = 'LAST_RESOURCE'
ENTITY_ALL_RESOURCES = '@ALL_RESOURCES@'
ENTITY_MAIN_RESOURCE = '@MAIN_RESOURCE@'
ENTITY_CAP_HASH = 'CAP_HASH'

View file

@ -94,5 +94,9 @@ class PasswordError(Exception):
pass
class PermissionError(Exception):
pass
class SkipHistory(Exception): # used in MessageReceivedTrigger to avoid history writting
pass

View file

@ -90,7 +90,7 @@ class SAT(service.Service):
self.bridge.register("disconnect", self.disconnect)
self.bridge.register("getContacts", self.getContacts)
self.bridge.register("getContactsFromGroup", self.getContactsFromGroup)
self.bridge.register("getLastResource", self.memory._getLastResource)
self.bridge.register("getMainResource", self.memory._getMainResource)
self.bridge.register("getPresenceStatuses", self.memory._getPresenceStatuses)
self.bridge.register("getWaitingSub", self.memory.getWaitingSub)
self.bridge.register("getWaitingConf", self.getWaitingConf)
@ -482,7 +482,7 @@ class SAT(service.Service):
# we may have a groupchat message, we check if the we know this jid
try:
entity_type = self.memory.getEntityData(mess_data["to"], ['type'], profile)["type"]
#FIXME: should entity_type manage ressources ?
#FIXME: should entity_type manage resources ?
except (exceptions.UnknownEntityError, KeyError):
entity_type = "contact"
@ -500,7 +500,7 @@ class SAT(service.Service):
if not self.trigger.point("sendMessage", mess_data, pre_xml_treatments, post_xml_treatments, profile):
return defer.succeed(None)
log.debug(_("Sending jabber message of type [%(type)s] to %(to)s...") % {"type": mess_data["type"], "to": to_jid.full()})
log.debug(_(u"Sending message (type {type}, to {to})").format(type=mess_data["type"], to=to_jid.full()))
def cancelErrorTrap(failure):
"""A message sending can be cancelled by a plugin treatment"""

View file

@ -102,9 +102,7 @@ class Discovery(object):
jid_ = jid.JID(client.jid.host)
try:
cap_hash = self.host.memory.getEntityData(jid_, [C.ENTITY_CAP_HASH], client.profile)[C.ENTITY_CAP_HASH]
disco_infos = self.hashes[cap_hash]
return defer.succeed(disco_infos)
except KeyError:
except (KeyError, exceptions.UnknownEntityError):
# capability hash is not available, we'll compute one
def infosCb(disco_infos):
cap_hash = self.generateHash(disco_infos)
@ -114,6 +112,9 @@ class Discovery(object):
d = client.disco.requestInfo(jid_)
d.addCallback(infosCb)
return d
else:
disco_infos = self.hashes[cap_hash]
return defer.succeed(disco_infos)
@defer.inlineCallbacks
def getItems(self, jid_=None, nodeIdentifier='', profile_key=C.PROF_KEY_NONE):
@ -131,7 +132,7 @@ class Discovery(object):
try:
items = self.host.memory.getEntityData(jid_, ["DISCO_ITEMS"], client.profile)["DISCO_ITEMS"]
log.debug("[%s] disco items are in cache" % jid_.full())
except KeyError:
except (KeyError, exceptions.UnknownEntityError):
log.debug("Caching [%s] disco items" % jid_.full())
items = yield client.disco.requestItems(jid_, nodeIdentifier)
self.host.memory.updateEntityData(jid_, "DISCO_ITEMS", items, client.profile)

View file

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

View file

@ -49,14 +49,14 @@ class Params(object):
<param name="%(show_empty_groups)s" label="%(show_empty_groups_label)s" value="true" type="bool" security="0" />
</category>
<category name="Connection" label="%(category_connection)s">
<param name="JabberID" value="name@example.org/SàT" type="string" />
<param name="JabberID" value="name@example.org/SàT" type="string" security="0" />
<param name="Password" value="" type="password" />
<param name="Priority" value="50" type="int" constraint="-128;127"/>
<param name="%(force_server_param)s" value="" type="string" />
<param name="%(force_port_param)s" value="" type="int" constraint="1;65535" />
<param name="NewAccount" label="%(new_account_label)s" type="button" callback_id="registerNewAccount"/>
<param name="autoconnect" label="%(autoconnect_label)s" value="true" type="bool" />
<param name="autodisconnect" label="%(autodisconnect_label)s" value="false" type="bool" />
<param name="autoconnect" label="%(autoconnect_label)s" value="true" type="bool" security="0" />
<param name="autodisconnect" label="%(autodisconnect_label)s" value="false" type="bool" security="0" />
</category>
<category name="Misc" label="%(category_misc)s">
<param name="Watched" value="test@Jabber.goffi.int" type="string" />
@ -473,12 +473,12 @@ class Params(object):
node = self._getParamNode(name, category)
if not node:
log.error(_("Requested param [%(name)s] in category [%(category)s] doesn't exist !") % {'name': name, 'category': category})
return defer.succeed(None)
raise ValueError("Requested param doesn't exist")
if not self.checkSecurityLimit(node[1], security_limit):
log.warning(_("Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!"
% {'param': name, 'cat': category}))
return defer.succeed(None)
raise exceptions.PermissionError
if node[0] == C.GENERAL:
value = self._getParam(category, name, C.GENERAL)

View file

@ -209,7 +209,7 @@ class TextCommands(object):
return False
if not target_jid.resource:
target_jid.resource = self.host.memory.getLastResource(target_jid, profile)
target_jid.resource = self.host.memory.getMainResource(target_jid, profile)
whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}]

View file

@ -29,8 +29,9 @@ from sat.tools import xml_tools
from twisted.words.protocols.jabber import jid
from twisted.python import failure
from twisted.internet import defer
import potr
from sat.memory import persistent
import potr
import copy
NS_OTR = "otr_plugin"
PRIVATE_KEY = "PRIVATE KEY"
@ -269,7 +270,7 @@ class OTR(object):
try:
to_jid = jid.JID(menu_data['jid'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
except KeyError:
log.error(_("jid key is not present !"))
return defer.fail(exceptions.DataError)
@ -287,7 +288,7 @@ class OTR(object):
try:
to_jid = jid.JID(menu_data['jid'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
except KeyError:
log.error(_("jid key is not present !"))
return defer.fail(exceptions.DataError)
@ -304,7 +305,7 @@ class OTR(object):
try:
to_jid = jid.JID(menu_data['jid'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
except KeyError:
log.error(_("jid key is not present !"))
return defer.fail(exceptions.DataError)
@ -374,7 +375,7 @@ class OTR(object):
try:
to_jid = jid.JID(menu_data['jid'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
except KeyError:
log.error(_("jid key is not present !"))
return defer.fail(exceptions.DataError)
@ -447,9 +448,9 @@ class OTR(object):
def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile):
if profile in self.skipped_profiles:
return True
to_jid = mess_data['to']
to_jid = copy.copy(mess_data['to'])
if mess_data['type'] != 'groupchat' and not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed
to_jid.resource = self.host.memory.getMainResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed
otrctx = self.context_managers[profile].getContextForUser(to_jid)
if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT:
if otrctx.state == potr.context.STATE_ENCRYPTED:
@ -475,7 +476,7 @@ class OTR(object):
if show != "unavailable":
return True
if not entity.resource:
entity.resource = self.host.memory.getLastResource(entity, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
entity.resource = self.host.memory.getMainResource(entity, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
otrctx = self.context_managers[profile].getContextForUser(entity)
otrctx.disconnect()
return True

View file

@ -85,6 +85,7 @@ class XEP_0054(object):
host.bridge.addMethod("getAvatarFile", ".plugin", in_sign='s', out_sign='s', method=self.getAvatarFile)
host.bridge.addMethod("setAvatar", ".plugin", in_sign='ss', out_sign='', method=self.setAvatar, async=True)
host.trigger.add("presence_available", self.presenceTrigger)
host.memory.setSignalOnUpdate("avatar")
def getHandler(self, profile):
return XEP_0054_handler(self)

View file

@ -55,7 +55,7 @@ class XEP_0060(object):
OPT_PUBLISH_MODEL = 'pubsub#publish_model'
def __init__(self, host):
log.info(_("PubSub plugin initialization"))
log.info(_(u"PubSub plugin initialization"))
self.host = host
self.managedNodes = []
self.clients = {}
@ -176,7 +176,7 @@ class XEP_0060(object):
d_dict = {}
for publisher, node in data.items():
if node not in found_nodes:
log.debug("Skip the items retrieval for [{node}]: node doesn't exist".format(node=node))
log.debug(u"Skip the items retrieval for [{node}]: node doesn't exist".format(node=node))
continue # avoid pubsub "item-not-found" error
d_dict[publisher] = client.items(service, node, max_items, item_ids, sub_id, client.parent.jid)
defer.returnValue(d_dict)
@ -222,7 +222,7 @@ class XEP_0060(object):
d_list = []
for nodeIdentifier in (set(nodeIdentifiers) - set(subscribed_nodes)):
if nodeIdentifier not in found_nodes:
log.debug("Skip the subscription to [{node}]: node doesn't exist".format(node=nodeIdentifier))
log.debug(u"Skip the subscription to [{node}]: node doesn't exist".format(node=nodeIdentifier))
continue # avoid sat-pubsub "SubscriptionExists" error
d_list.append(client.subscribe(service, nodeIdentifier, sub_jid or client.parent.jid.userhostJID(), options=options))
defer.returnValue(d_list)
@ -318,7 +318,7 @@ class SatPubSubClient(pubsub.PubSubClient):
def deleteReceived(self, event):
#TODO: manage delete event
log.debug(_("Publish node deleted"))
log.debug(_(u"Publish node deleted"))
# def purgeReceived(self, event):

View file

@ -39,6 +39,7 @@ MESSAGE_TYPES = ["chat", "groupchat"]
PARAM_KEY = "Notifications"
PARAM_NAME = "Enable chat state notifications"
ENTITY_KEY = PARAM_KEY + "_" + PARAM_NAME
DELETE_VALUE = "DELETE"
PLUGIN_INFO = {
"name": "Chat State Notifications Protocol Plugin",
@ -126,28 +127,31 @@ class XEP_0085(object):
del self.map[profile]
def updateEntityData(self, entity_jid, value, profile):
"""
Update the entity data of the given profile for one or all contacts.
"""Update the entity data of the given profile for one or all contacts.
Reset the chat state(s) display if the notification has been disabled.
@param entity_jid: contact's JID, or '@ALL@' to update all contacts.
@param value: True, False or C.PROF_KEY_NONE to delete the entity data
@param entity_jid: contact's JID, or C.ENTITY_ALL to update all contacts.
@param value: True, False or DELETE_VALUE to delete the entity data
@param profile: current profile
"""
self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile)
if not value or value == C.PROF_KEY_NONE:
# disable chat state for this or these contact(s)
self.host.bridge.chatStateReceived(unicode(entity_jid), "", profile)
if value == DELETE_VALUE:
self.host.memory.delEntityData(entity_jid, ENTITY_KEY, profile)
else:
self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile)
if not value or value == DELETE_VALUE:
# reinit chat state UI for this or these contact(s)
self.host.bridge.chatStateReceived(entity_jid.full(), "", profile)
def paramUpdateTrigger(self, name, value, category, type_, profile):
"""
Reset all the existing chat state entity data associated with this profile after a parameter modification.
"""Reset all the existing chat state entity data associated with this profile after a parameter modification.
@param name: parameter name
@param value: "true" to activate the notifications, or any other value to delete it
@param category: parameter category
@param type_: parameter type
"""
if (category, name) == (PARAM_KEY, PARAM_NAME):
self.updateEntityData("@ALL@", True if value == "true" else C.PROF_KEY_NONE, profile)
self.updateEntityData(C.ENTITY_ALL, True if bool("true") else DELETE_VALUE, profile)
return False
return True
@ -160,7 +164,7 @@ class XEP_0085(object):
return True
from_jid = JID(message.getAttribute("from"))
if self.__isMUC(from_jid, profile):
if self._isMUC(from_jid, profile):
from_jid = from_jid.userhostJID()
else: # update entity data for one2one chat
# assert from_jid.resource # FIXME: assert doesn't work on normal message from server (e.g. server announce), because there is no resource
@ -200,27 +204,27 @@ class XEP_0085(object):
def treatment(mess_data):
message = mess_data['xml']
to_jid = JID(message.getAttribute("to"))
if not self.__checkActivation(to_jid, forceEntityData=True, profile=profile):
if not self._checkActivation(to_jid, forceEntityData=True, profile=profile):
return mess_data
try:
# message with a body always mean active state
domish.generateElementsNamed(message.elements(), name="body").next()
message.addElement('active', NS_CHAT_STATES)
# launch the chat state machine (init the timer)
if self.__isMUC(to_jid, profile):
if self._isMUC(to_jid, profile):
to_jid = to_jid.userhostJID()
self.__chatStateActive(to_jid, mess_data["type"], profile)
except StopIteration:
if "chat_state" in mess_data["extra"]:
state = mess_data["extra"].pop("chat_state")
assert(state in CHAT_STATES)
assert state in CHAT_STATES
message.addElement(state, NS_CHAT_STATES)
return mess_data
post_xml_treatments.addCallback(treatment)
return True
def __isMUC(self, to_jid, profile):
def _isMUC(self, to_jid, profile):
"""Tell if that JID is a MUC or not
@param to_jid (JID): full or bare JID to check
@ -229,13 +233,13 @@ class XEP_0085(object):
"""
try:
type_ = self.host.memory.getEntityDatum(to_jid.userhostJID(), 'type', profile)
if type_ == 'chatroom':
if type_ == 'chatroom': # FIXME: should not use disco instead ?
return True
except (exceptions.UnknownEntityError, KeyError):
pass
return False
def __checkActivation(self, to_jid, forceEntityData, profile):
def _checkActivation(self, to_jid, forceEntityData, profile):
"""
@param to_jid: the contact's full JID (or bare if you know it's a MUC)
@param forceEntityData: if set to True, a non-existing
@ -247,9 +251,10 @@ class XEP_0085(object):
if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile):
return False
# check if notifications should be sent to this contact
if self.__isMUC(to_jid, profile):
if self._isMUC(to_jid, profile):
return True
assert to_jid.resource or not self.host.memory.isContactConnected(to_jid, profile)
# FIXME: this assertion crash when we want to send a message to an online bare jid
# assert to_jid.resource or not self.host.memory.isEntityAvailable(to_jid, profile) # must either have a resource, or talk to an offline contact
try:
return self.host.memory.getEntityDatum(to_jid, ENTITY_KEY, profile)
except (exceptions.UnknownEntityError, KeyError):
@ -305,11 +310,11 @@ class XEP_0085(object):
if profile is None:
raise exceptions.ProfileUnknownError
to_jid = JID(to_jid_s)
if self.__isMUC(to_jid, profile):
if self._isMUC(to_jid, profile):
to_jid = to_jid.userhostJID()
elif not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile)
if not self.__checkActivation(to_jid, forceEntityData=False, profile=profile):
to_jid.resource = self.host.memory.getMainResource(to_jid, profile)
if not self._checkActivation(to_jid, forceEntityData=False, profile=profile):
return
try:
self.map[profile][to_jid]._onEvent("composing")
@ -347,9 +352,9 @@ class ChatStateMachine:
notification to the contact (the "active" state is
automatically sent with each message) and set the timer.
"""
assert(state in TRANSITIONS)
assert state in TRANSITIONS
transition = TRANSITIONS[state]
assert("next_state" in transition and "delay" in transition)
assert "next_state" in transition and "delay" in transition
if state != self.state and state != "active":
if state != 'gone' or self.mess_type != 'groupchat':