core, plugin XEP-0115: discovery refactoring:

- hashing algorithm of XEP-0115 has been including in core
    - our own hash is still calculated by XEP-0115 and can be regenerated with XEP_0115.recalculateHash
    - old discovery methods have been removed. Now the following methods are used:
        - hasFeature: tell if a feature is available for an entity
        - getDiscoInfos: self explaining
        - getDiscoItems: self explaining
        - findServiceEntities: return all available items of an entity which given (category, type)
        - findFeaturesSet: search for a set of features in entity + entity's items
      all these methods are asynchronous, and manage cache automatically
    - XEP-0115 manage in a better way hashes, and now use a trigger for presence instead of monkey patch
    - new FeatureNotFound exception, when we want to do something which is not available
    - refactored client initialisation sequence, removed client.initialized Deferred
    - added constant APP_URL
    - test_plugin_xep_0033.py has been temporarly deactivated, the time to adapt it
    - lot of cleaning
This commit is contained in:
Goffi 2014-03-28 18:07:22 +01:00
commit 95678d7967
16 changed files with 431 additions and 479 deletions

View file

@ -28,6 +28,7 @@ class Const(object):
APP_NAME_SHORT = u'SàT'
APP_NAME_FULL = u'%s (%s)' % (APP_NAME_SHORT, APP_NAME)
APP_VERSION = u'0.4.1D' # Please add 'D' at the end for dev versions
APP_URL = 'http://www.salut-a-toi.org'
## Parameters ##
@ -40,7 +41,8 @@ class Const(object):
PROF_KEY_NONE = '@NONE@'
PROF_KEY_DEFAULT = '@DEFAULT@'
ENTITY_ALL = '@ALL@'
ENTITY_LAST_RESOURCE = 'last_resource'
ENTITY_LAST_RESOURCE = 'LAST_RESOURCE'
ENTITY_CAP_HASH = 'CAP_HASH'
## Configuration ##

View file

@ -74,6 +74,10 @@ class InternalError(Exception):
pass
class FeatureNotFound(Exception): # a disco feature/identity which is needed is not present
pass
class BridgeInitError(Exception):
pass

View file

@ -43,11 +43,6 @@ from sat.tools.misc import TriggerManager
from glob import glob
from uuid import uuid4
try:
from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
from wokkel.subprotocols import XMPPHandler
### logging configuration FIXME: put this elsewhere ###
logging.basicConfig(level=logging.DEBUG,
format='%(message)s')
@ -217,16 +212,6 @@ class SAT(service.Service):
info(_("already connected !"))
return defer.succeed("None")
if profile in self.profiles:
# avoid the following error when self.connect() is called twice for the same profile within a short time period:
# Jumping into debugger for post-mortem of exception ''SatXMPPClient' object has no attribute 'discoHandler'':
# > /usr/local/lib/python2.7/dist-packages/sat/plugins/plugin_xep_0115.py(151)generateHash()
# -> services = client.discoHandler.info(client.jid, client.jid, '').addCallback(generateHash_2, profile)
# This is a strange issue that is often happening on my system since libervia is being run as a twisted plugin.
# FIXME: properly find the problem an fix it
debug("being connected...")
return defer.succeed("None")
def afterMemoryInit(ignore):
"""This part must be called when we have loaded individual parameters from memory"""
try:
@ -262,17 +247,20 @@ class SAT(service.Service):
debug(_("setting plugins parents"))
plugin_conn_cb = []
for plugin in self.plugins.iteritems():
if plugin[1].is_handler:
plugin[1].getHandler(profile).setHandlerParent(current)
connected_cb = getattr(plugin[1], "profileConnected", None)
if connected_cb:
connected_cb(profile)
plugin_conn_cb.append(connected_cb)
current.startService()
d = current.getConnectionDeferred()
d.addCallback(lambda x: current.roster.got_roster) # we want to be sure that we got the roster
d.addCallback(lambda dummy: current.roster.got_roster) # we want to be sure that we got the roster
for callback in plugin_conn_cb:
d.addCallback(lambda dummy: callback(profile))
return d
self.memory.startProfileSession(profile)
@ -360,14 +348,6 @@ class SAT(service.Service):
raise exceptions.ProfileKeyUnknownError
return [self.profiles[profile]]
def getClientHostJid(self, profile_key):
"""Convenient method to get the client host from profile key
@return: host jid or None if it doesn't exist"""
profile = self.memory.getProfileName(profile_key)
if not profile:
return None
return self.profiles[profile].getHostJid()
def registerNewAccount(self, login, password, email, server, port=5222, id_=None, profile_key=C.PROF_KEY_NONE):
"""Connect to a server and create a new account using in-band registration"""
profile = self.memory.getProfileName(profile_key)
@ -623,113 +603,26 @@ class SAT(service.Service):
self.profiles[profile].roster.removeItem(to_jid)
self.profiles[profile].presence.unsubscribe(to_jid)
def requestServerDisco(self, feature, jid_=None, cache_only=False, profile_key="@NONE"):
"""Discover if a server or its items offer a given feature
@param feature: the feature to check
@param jid_: the jid of the server, local server if None
@param cache_only: expect the result to be in cache and don't actually make any request.
This can be used anytime for requesting a feature on the local server because the data are cached for sure.
@result: the Deferred entity jid offering the feature, or None
"""
profile = self.memory.getProfileName(profile_key)
## Discovery ##
# discovery methods are shortcuts to self.memory.disco
# the main difference with client.disco is that self.memory.disco manage cache
if not profile:
return defer.succeed(None)
if jid_ is None:
jid_ = self.getClientHostJid(profile)
cache_only = True
hasServerFeature = lambda entity: entity if self.memory.hasServerFeature(feature, entity, profile) else None
def hasFeature(self, *args, **kwargs):
return self.memory.disco.hasFeature(*args, **kwargs)
def haveItemsFeature(dummy=None):
entities = self.memory.getAllServerIdentities(jid_, profile)
if entities is None:
return None # no cached data for this server
for entity in entities:
if hasServerFeature(entity):
return entity
return None # data are cached but no entity was found
def getDiscoInfos(self, *args, **kwargs):
return self.memory.disco.getInfos(*args, **kwargs)
entity = hasServerFeature(jid_) or haveItemsFeature()
if entity:
return defer.succeed(entity)
elif entity is False or cache_only:
return defer.succeed(None)
def getDiscoItems(self, *args, **kwargs):
return self.memory.disco.getItems(*args, **kwargs)
# data for this server are not in cache
disco = self.profiles[profile].disco
def findServiceEntities(self, *args, **kwargs):
return self.memory.disco.findServiceEntities(*args, **kwargs)
def errback(failure, method, jid_, profile):
# the target server is not reachable
logging.error("disco.%s on %s failed! [%s]" % (method.func_name, jid_.userhost(), profile))
logging.error("reason: %s" % failure.getErrorMessage())
if method == disco.requestInfo:
features = self.memory.server_features.setdefault(profile, {})
features.setdefault(jid_, [])
elif method == disco.requestItems:
identities = self.memory.server_identities.setdefault(profile, {})
identities.setdefault(jid_, {})
return failure
def findFeaturesSet(self, *args, **kwargs):
return self.memory.disco.findFeaturesSet(*args, **kwargs)
def callback(d):
if hasServerFeature(jid_):
return jid_
else:
d2 = disco.requestItems(jid_).addCallback(self.serverDiscoItems, disco, jid_, profile)
d2.addErrback(errback, disco.requestItems, jid_, profile)
return d2.addCallback(haveItemsFeature)
d = disco.requestInfo(jid_).addCallback(self.serverDisco, jid_, profile)
d.addCallbacks(callback, errback, [], errbackArgs=[disco.requestInfo, jid_, profile])
return d
## callbacks ##
def serverDisco(self, disco, jid_=None, profile=None):
"""xep-0030 Discovery Protocol.
@param disco: result of the disco info query
@param jid_: the jid of the target server
@param profile: profile of the user
"""
if jid_ is None:
jid_ = self.getClientHostJid(profile)
debug(_("Requested disco info on %s") % jid_)
for feature in disco.features:
debug(_("Feature found: %s") % feature)
self.memory.addServerFeature(feature, jid_, profile)
for cat, type_ in disco.identities:
debug(_("Identity found: [%(category)s/%(type)s] %(identity)s")
% {'category': cat, 'type': type_, 'identity': disco.identities[(cat, type_)]})
def serverDiscoItems(self, disco_result, disco_client, jid_, profile, initialized=None):
"""xep-0030 Discovery Protocol.
@param disco_result: result of the disco item querry
@param disco_client: SatDiscoProtocol instance
@param jid_: the jid of the target server
@param profile: profile of the user
@param initialized: deferred which must be chained when everything is done"""
def _check_entity_cb(result, entity, jid_, profile):
debug(_("Requested disco info on %s") % entity)
for category, type_ in result.identities:
debug(_('Identity added: (%(category)s,%(type)s) ==> %(entity)s [%(profile)s]')
% {'category': category, 'type': type_, 'entity': entity, 'profile': profile})
self.memory.addServerIdentity(category, type_, entity, jid_, profile)
for feature in result.features:
self.memory.addServerFeature(feature, entity, profile)
def _errback(result, entity, jid_, profile):
warning(_("Can't get information on identity [%(entity)s] for profile [%(profile)s]") % {'entity': entity, 'profile': profile})
defer_list = []
for item in disco_result._items:
if item.entity.full().count('.') == 1: # XXX: workaround for a bug on jabberfr, tmp
warning(_('Using jabberfr workaround, be sure your domain has at least two levels (e.g. "example.tld", not "example" alone)'))
continue
args = [item.entity, jid_, profile]
defer_list.append(disco_client.requestInfo(item.entity).addCallbacks(_check_entity_cb, _errback, args, None, args))
if initialized:
defer.DeferredList(defer_list).chainDeferred(initialized)
## Generic HMI ##

View file

@ -41,7 +41,6 @@ class SatXMPPClient(client.XMPPClient):
self.__connected = False
self.profile = profile
self.host_app = host_app
self.client_initialized = defer.Deferred()
self.conn_deferred = defer.Deferred()
self._waiting_conf = {} # callback called when a confirmation is received
self._progress_cb_map = {} # callback called when a progress is requested (key = progress id)
@ -69,19 +68,17 @@ class SatXMPPClient(client.XMPPClient):
self.disco.setHandlerParent(self)
self.discoHandler = disco.DiscoHandler()
self.discoHandler.setHandlerParent(self)
disco_d = defer.succeed(None)
if not self.host_app.trigger.point("Disco Handled", self.profile):
if not self.host_app.trigger.point("Disco handled", disco_d, self.profile):
return
self.roster.requestRoster()
def finish_connection(dummy):
self.roster.requestRoster()
self.presence.available()
self.conn_deferred.callback(None)
self.presence.available()
jid_ = self.getHostJid()
self.disco.requestInfo(jid_).addCallback(self.host_app.serverDisco, jid_, self.profile) # FIXME: use these informations
self.disco.requestItems(jid_).addCallback(self.host_app.serverDiscoItems, self.disco, jid_, self.profile, self.client_initialized)
self.conn_deferred.callback(None)
disco_d.addCallback(finish_connection)
def initializationFailed(self, reason):
error(_("ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
@ -106,10 +103,6 @@ class SatXMPPClient(client.XMPPClient):
self.host_app.bridge.disconnected(self.profile) # we send the signal to the clients
self.host_app.purgeClient(self.profile) # and we remove references to this client
def getHostJid(self):
"""@return: the jid of the local server"""
return jid.JID(self.jid.host)
class SatMessageProtocol(xmppim.MessageProtocol):
@ -292,6 +285,11 @@ class SatPresenceProtocol(xmppim.PresenceClientProtocol):
xmppim.PresenceClientProtocol.__init__(self)
self.host = host
def send(self, obj):
if not self.host.trigger.point("Presence send", obj):
return
super(SatPresenceProtocol, self).send(obj)
def availableReceived(self, entity, show=None, statuses=None, priority=0):
debug(_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity': entity, 'show': show, 'statuses': statuses, 'priority': priority})
@ -434,7 +432,7 @@ class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
if self.user_email:
_email = query.addElement('email')
_email.addContent(self.user_email)
reg = iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure)
iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure)
def registrationAnswer(self, answer):
debug(_("registration answer: %s") % answer.toXml())

197
src/memory/disco.py Executable file
View file

@ -0,0 +1,197 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# SAT: a jabber client
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core.i18n import _
from logging import debug, info, warning, error
from twisted.words.protocols.jabber import jid
from twisted.internet import defer
from sat.core.constants import Const as C
from wokkel import disco
from base64 import b64encode
from hashlib import sha1
PRESENCE = '/presence'
NS_ENTITY_CAPABILITY = 'http://jabber.org/protocol/caps'
CAPABILITY_UPDATE = PRESENCE + '/c[@xmlns="' + NS_ENTITY_CAPABILITY + '"]'
class HashGenerationError(Exception):
pass
class ByteIdentity(object):
"""This class manage identity as bytes (needed for i;octet sort), it is used for the hash generation"""
def __init__(self, identity, lang=None):
assert isinstance(identity, disco.DiscoIdentity)
self.category = identity.category.encode('utf-8')
self.idType = identity.type.encode('utf-8')
self.name = identity.name.encode('utf-8') if identity.name else ''
self.lang = lang.encode('utf-8') if lang is not None else ''
def __str__(self):
return "%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name)
class Discovery(object):
""" Manage capabilities of entities """
def __init__(self, host):
self.host = host
self.hashes = {} # key: capability hash, value: disco features/identities
# TODO: save hashes in databse, remove legacy hashes
@defer.inlineCallbacks
def hasFeature(self, feature, jid_=None, profile_key=C.PROF_KEY_NONE):
"""Tell if an entity has the required feature
@param feature: feature namespace
@param jid_: jid of the target, or None for profile's server
@param profile_key: %(doc_profile_key)s
@return: a Deferred which fire a boolean (True if feature is available)
"""
disco_info = yield self.getInfos(jid_, profile_key)
defer.returnValue(feature in disco_info)
@defer.inlineCallbacks
def getInfos(self, jid_=None, profile_key=C.PROF_KEY_NONE):
"""get disco infos from jid_, filling capability hash if needed
@param jid_: jid of the target, or None for profile's server
@param profile_key: %(doc_profile_key)s
@return: a Deferred which fire disco.DiscoInfo
"""
client = self.host.getClient(profile_key)
if jid_ is None:
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_info = self.hashes[cap_hash]
except KeyError:
# capability hash is not available, we'll compute one
disco_info = yield client.disco.requestInfo(jid_)
cap_hash = self.generateHash(disco_info)
self.hashes[cap_hash] = disco_info
yield self.host.memory.updateEntityData(jid_, C.ENTITY_CAP_HASH, cap_hash, client.profile)
defer.returnValue(disco_info)
@defer.inlineCallbacks
def getItems(self, jid_=None, profile_key=C.PROF_KEY_NONE):
"""get disco items from jid_, cache them for our own server
@param jid_: jid of the target, or None for profile's server
@param profile_key: %(doc_profile_key)s
@return: a Deferred which fire disco.DiscoInfo
"""
client = self.host.getClient(profile_key)
if jid_ is None:
jid_ = jid.JID(client.jid.host)
# we cache items only for our own server
try:
items = self.host.memory.getEntityData(jid_, ["DISCO_ITEMS"], client.profile)["DISCO_ITEMS"]
debug("[%s] disco items are in cache" % jid_.full())
except KeyError:
debug("Caching [%s] disco items" % jid_.full())
items = yield client.disco.requestItems(jid_)
self.host.memory.updateEntityData(jid_, "DISCO_ITEMS", items, client.profile)
else:
items = yield client.disco.requestItems(jid_)
defer.returnValue(items)
@defer.inlineCallbacks
def findServiceEntities(self, category, type_, jid_=None, profile_key=C.PROF_KEY_NONE):
"""Return all available items of an entity which correspond to (category, type_)
@param category: identity's category
@param type_: identitiy's type
@param jid_: the jid of the target server (None for profile's server)
@param profile_key: %(doc_profile_key)s
@return: a set of entities or None if no cached data were found
"""
found_identities = set()
items = yield self.getItems(jid_, profile_key)
for item in items:
infos = yield self.getInfos(item.entity, profile_key)
if (category, type_) in infos.identities:
found_identities.add(item.entity)
defer.returnValue(found_identities)
@defer.inlineCallbacks
def findFeaturesSet(self, features, category=None, type_=None, jid_=None, profile_key=C.PROF_KEY_NONE):
"""Return entities (including jid_ and its items) offering features
@param features: iterable of features which must be present
@param category: if not None, accept only this category
@param type_: if not None, accept only this type
@param jid_: the jid of the target server (None for profile's server)
@param profile_key: %(doc_profile_key)s
@return: a set of found entities
"""
client = self.host.getClient(profile_key)
if jid_ is None:
jid_ = jid.JID(client.jid.host)
features = set(features)
found_entities = set()
items = yield self.getItems(jid_, profile_key)
for entity in [jid_] + items:
infos = yield self.getInfos(entity, profile_key)
if category is not None or type_ is not None:
categories = set()
types = set()
for identity in infos.identities:
id_cat, id_type = identity
categories.add(id_cat)
types.add(id_type)
if category is not None and category not in categories:
continue
if type_ is not None and type_ not in types:
continue
if features.issubset(infos.features):
found_entities.add(entity)
defer.returnValue(found_entities)
def generateHash(self, services):
""" Generate a unique hash for given service
hash algorithm is the one described in XEP-0115
@param services: iterable of disco.DiscoIdentity/disco.DiscoFeature, as returned by discoHandler.info
"""
s = []
byte_identities = [ByteIdentity(service) for service in services if isinstance(service, disco.DiscoIdentity)] # FIXME: lang must be managed here
byte_identities.sort(key=lambda i: i.lang)
byte_identities.sort(key=lambda i: i.idType)
byte_identities.sort(key=lambda i: i.category)
for identity in byte_identities:
s.append(str(identity))
s.append('<')
byte_features = [service.encode('utf-8') for service in services if isinstance(service, disco.DiscoFeature)]
byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort
for feature in byte_features:
s.append(feature)
s.append('<')
#TODO: manage XEP-0128 data form here
cap_hash = b64encode(sha1(''.join(s)).digest())
debug(_('Capability hash generated: [%s]') % cap_hash)
return cap_hash

View file

@ -32,6 +32,7 @@ from sat.core.constants import Const as C
from sat.memory.sqlite import SqliteStorage
from sat.memory.persistent import PersistentDict
from sat.memory.params import Params
from sat.memory.disco import Discovery
class Sessions(object):
@ -115,8 +116,7 @@ class Memory(object):
self._entities_cache = {} # XXX: keep presence/last resource/other data in cache
# /!\ an entity is not necessarily in roster
self.subscriptions = {}
self.server_features = {} # used to store discovery's informations
self.server_identities = {}
self.disco = Discovery(host)
self.config = self.parseMainConf()
self.__fixLocalDir()
database_file = os.path.expanduser(os.path.join(self.getConfig('', 'local_dir'), C.SAVEFILE_DATABASE))
@ -280,93 +280,6 @@ class Memory(object):
assert profile != C.PROF_KEY_NONE
return self.storage.getHistory(jid.JID(from_jid), jid.JID(to_jid), limit, between, profile)
def addServerFeature(self, feature, jid_, profile):
"""Add a feature discovered from server
@param feature: string of the feature
@param jid_: the jid of the target server
@param profile: which profile asked this server?"""
if profile not in self.server_features:
self.server_features[profile] = {}
features = self.server_features[profile].setdefault(jid_, [])
features.append(feature)
def addServerIdentity(self, category, type_, entity, jid_, profile):
"""Add an identity discovered from server
@param feature: string of the feature
@param jid_: the jid of the target server
@param profile: which profile asked this server?"""
if not profile in self.server_identities:
self.server_identities[profile] = {}
identities = self.server_identities[profile].setdefault(jid_, {})
if (category, type_) not in identities:
identities[(category, type_)] = set()
identities[(category, type_)].add(entity)
def getServerServiceEntities(self, category, type_, jid_=None, profile=None):
"""Return all available entities of a server for the service (category, type_)
@param category: identity's category
@param type_: identitiy's type
@param jid_: the jid of the target server (None for profile's server)
@param profile: which profile is asking this server?
@return: a set of entities or None if no cached data were found
"""
if jid_ is None:
jid_ = self.host.getClientHostJid(profile)
if profile in self.server_identities and jid_ in self.server_identities[profile]:
return self.server_identities[profile][jid_].get((category, type_), set())
else:
return None
def getServerServiceEntity(self, category, type_, jid_=None, profile=None):
"""Helper method to get first available entity of a server for the service (category, type_)
@param category: identity's category
@param type_: identitiy's type
@param jid_: the jid of the target server (None for profile's server)
@param profile: which profile is asking this server?
@return: the first found entity or None if no cached data were found
"""
entities = self.getServerServiceEntities(category, type_, jid_, profile)
if entities is None:
warning(_("Entities (%(category)s/%(type)s) of %(server)s not available, maybe they haven't been asked yet?")
% {"category": category, "type": type_, "server": jid_})
return None
else:
return list(entities)[0] if entities else None
def getAllServerIdentities(self, jid_, profile):
"""Helper method to get all identities of a server
@param jid_: the jid of the target server (None for profile's server)
@param profile: which profile is asking this server?
@return: a set of entities or None if no cached data were found
"""
if jid_ is None:
jid_ = self.host.getClientHostJid(profile)
if jid_ not in self.server_identities[profile]:
return None
entities = set()
for set_ in self.server_identities[profile][jid_].values():
entities.update(set_)
return entities
def hasServerFeature(self, feature, jid_=None, profile_key=C.PROF_KEY_NONE):
"""Tell if the specified server has the required feature
@param feature: requested feature
@param jid_: the jid of the target server (None for profile's server)
@param profile_key: %(doc_profile_key)s
"""
profile = self.getProfileName(profile_key)
if not profile:
error(_('Trying find server feature for a non-existant profile'))
return None
assert profile in self.server_features
if jid_ is None:
jid_ = self.host.getClientHostJid(profile)
if jid_ in self.server_features[profile]:
return feature in self.server_features[profile][jid_]
else:
warning(_("Features of %s not available, maybe they haven't been asked yet?") % jid_)
return None
def _getLastResource(self, jid_s, profile_key):
jid_ = jid.JID(jid_s)
return self.getLastResource(jid_, profile_key) or ""
@ -484,8 +397,9 @@ class Memory(object):
def getEntityData(self, entity_jid, keys_list, profile_key):
"""Get a list of cached values for entity
@param entity_jid: JID of the entity
@param keys_list: list of keys to get, empty list for everything
@param keys_list (iterable): list of keys to get, empty list 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
@ -504,6 +418,20 @@ class Memory(object):
ret[key] = entity_data[key]
return ret
def getEntityDatum(self, entity_jid, key, profile_key):
"""Get a datum from entity
@param entity_jid: JID of the entity
@param keys: key to get
@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
"""
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
@param entity_jid: JID of the entity to delete

View file

@ -146,6 +146,7 @@ class Params(object):
def asyncDeleteProfile(self, profile, force=False):
"""Delete an existing profile
@param profile: name of the profile
@param force: force the deletion even if the profile is connected.
To be used for direct calls only (not through the bridge).
@ -164,6 +165,7 @@ class Params(object):
def getProfileName(self, profile_key, return_profile_keys = False):
"""return profile according to profile_key
@param profile_key: profile name or key which can be
@ALL@ for all profiles
@DEFAULT@ for default profile

View file

@ -154,26 +154,18 @@ class GroupBlog(object):
raise exceptions.ProfileUnknownError
client = self.host.getClient(profile)
yield client.client_initialized # we want to be sure that the client is initialized
#we first check that we have a item-access pubsub server
if not hasattr(client, "item_access_pubsub"):
debug(_('Looking for item-access power pubsub server'))
debug(_('Looking for item-access powered pubsub server'))
#we don't have any pubsub server featuring item access yet
client.item_access_pubsub = None
client._item_access_pubsub_pending = defer.Deferred()
for entity in self.host.memory.getServerServiceEntities("pubsub", "service", profile=profile):
_disco = yield client.disco.requestInfo(entity)
#if set([NS_PUBSUB_ITEM_ACCESS, NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK]).issubset(_disco.features):
if set([NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK]).issubset(_disco.features):
info(_("item-access powered pubsub service found: [%s]") % entity.full())
client.item_access_pubsub = entity
client._item_access_pubsub_pending.callback(None)
if hasattr(client, "_item_access_pubsub_pending"):
#XXX: we need to wait for item access pubsub service check
yield client._item_access_pubsub_pending
del client._item_access_pubsub_pending
item_access_pubsubs = yield self.host.findFeaturesSet((NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK), "pubsub", "service", profile_key=profile)
# item_access_pubsubs = yield self.host.findFeaturesSet((NS_PUBSUB_ITEM_ACCESS, NS_PUBSUB_AUTO_CREATE, NS_PUBSUB_CREATOR_JID_CHECK), "pubsub", "service", profile_key=profile)
try:
client.item_access_pubsub = item_access_pubsubs.pop()
info(_("item-access powered pubsub service found: [%s]") % client.item_access_pubsub.full())
except KeyError:
client.item_access_pubsub = None
if not client.item_access_pubsub:
error(_("No item-access powered pubsub server found, can't use group blog"))

View file

@ -362,6 +362,7 @@ class RoomGame(object):
def _checkWaitAuth(self, room, other_players, verbose=False):
"""Check if we must wait for other players before starting the game.
@param room: wokkel.muc.Room instance
@param other_players: list of players JID userhosts without the referee
@param verbose: display debug message
@ -383,17 +384,20 @@ class RoomGame(object):
debug(_("Still waiting for %(users)s before starting the game %(game)s in %(room)s") % {'users': result[2], 'game': self.name, 'room': room.occupantJID.userhost()})
return result
def getUniqueName(self, muc_service="", profile_key=C.PROF_KEY_NONE):
"""
def getUniqueName(self, muc_service=None, profile_key=C.PROF_KEY_NONE):
"""Generate unique room name
@param muc_service: you can leave empty to autofind the muc service
@param profile_key
@param profile_key: %(doc_profile_key)s
@return: a unique name for a new room to be created
"""
# FIXME: jid.JID must be used instead of strings
room = self.host.plugins["XEP-0045"].getUniqueName(muc_service, profile_key=profile_key)
return "sat_%s_%s" % (self.name.lower(), room) if room != "" else ""
return "sat_%s_%s" % (self.name.lower(), room.full())
def prepareRoom(self, other_players=None, room_jid_s=None, profile_key=C.PROF_KEY_NONE):
"""Prepare the room for a game: create it if it doesn't exist and invite players.
@param other_players: list for other players JID userhosts
@param room_jid_s: JID userhost of the room, or None to generate a unique name
@param profile_key
@ -410,28 +414,22 @@ class RoomGame(object):
"""@param room: instance of wokkel.muc.Room"""
self._createOrInvite(room, [JID(player).userhost() for player in other_players], profile)
def afterClientInit(room_jid_s):
"""Create/join the given room, or a unique generated one if no room is specified.
@param room_jids: userhost of the room to join
"""
if room_jid_s is not None and room_jid_s != "": # a room name has been specified
if room_jid_s in self.host.plugins["XEP-0045"].clients[profile].joined_rooms:
roomJoined(self.host.plugins["XEP-0045"].clients[profile].joined_rooms[room_jid_s])
return
else:
room_jid_s = self.getUniqueName(profile_key=profile_key)
if room_jid_s == "":
return
user_jid = self.host.getJidNStream(profile)[0]
d = self.host.plugins["XEP-0045"].join(JID(room_jid_s), user_jid.user, {}, profile)
d.addCallback(roomJoined)
client = self.host.getClient(profile)
client.client_initialized.addCallback(lambda ignore: afterClientInit(room_jid_s))
# Create/join the given room, or a unique generated one if no room is specified.
if room_jid_s is not None and room_jid_s != "": # a room name has been specified
if room_jid_s in self.host.plugins["XEP-0045"].clients[profile].joined_rooms:
roomJoined(self.host.plugins["XEP-0045"].clients[profile].joined_rooms[room_jid_s])
return
else:
room_jid_s = self.getUniqueName(profile_key=profile_key)
if room_jid_s == "":
return
user_jid = self.host.getJidNStream(profile)[0]
d = self.host.plugins["XEP-0045"].join(JID(room_jid_s), user_jid.user, {}, profile)
return d.addCallback(roomJoined)
def userJoinedTrigger(self, room, user, profile):
"""This trigger is used to check if the new user can take part of a game,
create the game if we were waiting for him or just update the players list.
"""This trigger is used to check if the new user can take part of a game, create the game if we were waiting for him or just update the players list.
@room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User}
@user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID
@return: True to not interrupt the main process.
@ -463,6 +461,7 @@ class RoomGame(object):
def userLeftTrigger(self, room, user, profile):
"""This trigger is used to update or stop the game when a user leaves.
@room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User}
@user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID
@return: True to not interrupt the main process.
@ -491,13 +490,13 @@ class RoomGame(object):
return True
def _checkCreateGameAndInit(self, room_jid_s, profile):
"""Check if that profile can create the game. If the game can be created
but is not initialized yet, this method will also do the initialization.
"""Check if that profile can create the game. If the game can be created but is not initialized yet, this method will also do the initialization.
@param room_jid_s: room userhost
@param profile
@return: a couple (create, sync) with:
- create: set to True to allow the game creation
- sync: set to True to advice a game synchronization
- create: set to True to allow the game creation
- sync: set to True to advice a game synchronization
"""
user_nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid_s, profile)
if not user_nick:
@ -516,12 +515,14 @@ class RoomGame(object):
return True, False
def createGame(self, room_jid_s, nicks=None, profile_key=C.PROF_KEY_NONE):
"""Create a new game - this can be called directly from a frontend
and skips all the checks and invitation system, but the game must
not exist and all the players must be in the room already.
"""Create a new game.
This can be called directly from a frontend and skips all the checks and invitation system,
but the game must not exist and all the players must be in the room already.
@param room_jid: JID userhost of the room
@param nicks: list of players nicks in the room (referee included, in first position)
@param profile_key: %(doc_profile_key)s"""
@param profile_key: %(doc_profile_key)s
"""
debug(_("Creating %(game)s game in room %(room)s") % {'game': self.name, 'room': room_jid_s})
profile = self.host.memory.getProfileName(profile_key)
if not profile:
@ -545,6 +546,7 @@ class RoomGame(object):
def playerReady(self, player, referee, profile_key=C.PROF_KEY_NONE):
"""Must be called when player is ready to start a new game
@param player: the player nick in the room
@param referee: referee userhost
"""
@ -558,10 +560,11 @@ class RoomGame(object):
def newRound(self, room_jid, data, profile):
"""Launch a new round (reinit the user data)
@param room_jid: room userhost
@param data: a couple (common_data, msg_elts) with:
- common_data: backend initialization data for the new round
- msg_elts: dict to map each user to his specific initialization message
- common_data: backend initialization data for the new round
- msg_elts: dict to map each user to his specific initialization message
@param profile
"""
debug(_('new round for %s game') % self.name)
@ -585,6 +588,7 @@ class RoomGame(object):
def _createGameElt(self, to_jid):
"""Create a generic domish Element for the game messages
@param to_jid: JID of the recipient
@return: the created element
"""
@ -597,10 +601,11 @@ class RoomGame(object):
def _createStartElement(self, players=None, name="started"):
"""Create a domish Element listing the game users
@param players: list of the players
@param name: element name:
- "started" to signal the players that the game has been started
- "players" to signal the list of players when the game is not started yet
- "started" to signal the players that the game has been started
- "players" to signal the list of players when the game is not started yet
@return the create element
"""
started_elt = domish.Element((None, name))
@ -616,15 +621,16 @@ class RoomGame(object):
return started_elt
def _sendElements(self, to_jid, data, profile=None):
"""
""" TODO
@param to_jid: recipient JID
@param data: list of (elem, attr, content) with:
- elem: domish.Element, unicode or a couple:
- domish.Element to be directly added as a child to the message
- unicode name or couple (uri, name) to create a new domish.Element
and add it as a child to the message (see domish.Element.addElement)
- attrs: dictionary of attributes for the new child
- content: unicode that is appended to the child content
- elem: domish.Element, unicode or a couple:
- domish.Element to be directly added as a child to the message
- unicode name or couple (uri, name) to create a new domish.Element
and add it as a child to the message (see domish.Element.addElement)
- attrs: dictionary of attributes for the new child
- content: unicode that is appended to the child content
@param profile: the profile from which the message is sent
@return: a Deferred instance
"""
@ -646,12 +652,13 @@ class RoomGame(object):
return defer.succeed(None)
def send(self, to_jid, elem=None, attrs=None, content=None, profile=None):
"""
""" TODO
@param to_jid: recipient JID
@param elem: domish.Element, unicode or a couple:
- domish.Element to be directly added as a child to the message
- unicode name or couple (uri, name) to create a new domish.Element
and add it as a child to the message (see domish.Element.addElement)
- domish.Element to be directly added as a child to the message
- unicode name or couple (uri, name) to create a new domish.Element
and add it as a child to the message (see domish.Element.addElement)
@param attrs: dictionary of attributes for the new child
@param content: unicode that is appended to the child content
@param profile: the profile from which the message is sent

View file

@ -32,11 +32,6 @@ from wokkel import muc
from sat.tools import xml_tools
try:
from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
from wokkel.subprotocols import XMPPHandler
PLUGIN_INFO = {
"name": "XEP 0045 Plugin",
"import_name": "XEP-0045",
@ -53,6 +48,8 @@ PLUGIN_INFO = {
class UnknownRoom(Exception):
pass
class NotReadyYet(Exception):
pass
class XEP_0045(object):
# TODO: this plugin is messy, need a big cleanup/refactoring
@ -67,7 +64,7 @@ class XEP_0045(object):
host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self.mucLeave, async=True)
host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sass)', method=self.getRoomsJoined)
host.bridge.addMethod("getRoomsSubjects", ".plugin", in_sign='s', out_sign='a(ss)', method=self.getRoomsSubjects)
host.bridge.addMethod("getUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self.getUniqueName)
host.bridge.addMethod("getUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName)
host.bridge.addMethod("configureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True)
host.bridge.addSignal("roomJoined", ".plugin", signature='sasss') # args: room_jid, room_nicks, user_nick, profile
host.bridge.addSignal("roomLeft", ".plugin", signature='ss') # args: room_jid, profile
@ -82,8 +79,15 @@ class XEP_0045(object):
except KeyError:
info(_("Text commands not available"))
def profileConnected(self, profile):
def assign_service(service):
client = self.host.getClient(profile)
client.muc_service = service
self.getMUCService(profile_key=profile).addCallback(assign_service)
def __check_profile(self, profile):
"""check if profile is used and connected
if profile known but disconnected, remove it from known profiles
@param profile: profile to check
@return: True if the profile is known and connected, else False"""
@ -136,6 +140,7 @@ class XEP_0045(object):
def getRoomNick(self, room_jid_s, profile_key=C.PROF_KEY_NONE):
"""return nick used in room by user
@param room_jid_s: unicode room id
@profile_key: profile
@return: nick or empty string in case of error"""
@ -146,6 +151,7 @@ class XEP_0045(object):
def getRoomNickOfUser(self, room, user_jid, secure=True):
"""Returns the nick of the given user in the room.
@room: instance of wokkel.muc.Room
@user: JID or unicode (JID userhost).
@param secure: set to True for a secure check
@ -166,6 +172,7 @@ class XEP_0045(object):
def getRoomNicksOfUsers(self, room, users=[], secure=True):
"""Returns the nicks of the given users in the room.
@room: instance of wokkel.muc.Room
@users: list of JID or unicode (JID userhost).
@param secure: set to True for a secure check
@ -189,10 +196,10 @@ class XEP_0045(object):
def configureRoom(self, room_jid, profile_key=C.PROF_KEY_NONE):
""" return the room configuration form
@param room: jid of the room to configure
@param profile_key: %(doc_profile_key)s
@return: configuration form as XMLUI
"""
profile = self.host.memory.getProfileName(profile_key)
if not self.__check_profile(profile):
@ -243,38 +250,47 @@ class XEP_0045(object):
return []
return self.clients[profile].rec_subjects.values()
def getMUCService(self, profile):
"""Return the MUC service or None"""
@defer.inlineCallbacks
def getMUCService(self, jid_=None, profile_key=C.PROF_KEY_NONE):
"""Return first found MUC service of an entity
@param jid_: entity which may have a MUC service, or None for our own server
@param profile_key: %(doc_profile_key)s
"""
muc_service = None
for service in self.host.memory.getServerServiceEntities("conference", "text", profile=profile):
services = yield self.host.findServiceEntities("conference", "text", jid_, profile_key=profile_key)
for service in services:
if not ".irc." in service.userhost():
#FIXME:
#This awfull ugly hack is here to avoid an issue with openfire: the irc gateway
#use "conference/text" identity (instead of "conference/irc"), there is certainly a better way
#to manage this, but this hack fill do it for test purpose
# FIXME:
# This ugly hack is here to avoid an issue with openfire: the IRC gateway
# use "conference/text" identity (instead of "conference/irc")
muc_service = service
break
return muc_service
defer.returnValue(muc_service)
def _getUniqueName(self, muc_service="", profile_key=C.PROF_KEY_NONE):
return self.getUniqueName(muc_service or None, profile_key).full()
def getUniqueName(self, muc_service=None, profile_key=C.PROF_KEY_NONE):
"""Return unique name for a room, avoiding collision
def getUniqueName(self, muc_service="", profile_key=C.PROF_KEY_NONE):
"""Return unique name for room, avoiding collision
@param muc_service: leave empty string to use the default service
@return: unique room userhost, or '' if an error occured.
"""
#TODO: we should use #RFC-0045 10.1.4 when available here
profile = self.host.memory.getProfileName(profile_key)
if not profile:
error(_("Unknown profile"))
return ""
client = self.host.getClient(profile_key)
room_name = uuid.uuid1()
print "\n\n===> room_name:", room_name
if muc_service == "":
muc_service = self.getMUCService(profile)
if not muc_service:
error(_("Can't find a MUC service"))
return ""
muc_service = muc_service.userhost()
return "%s@%s" % (room_name, muc_service)
if muc_service is None:
try:
muc_service = client.muc_service
except AttributeError:
raise NotReadyYet("Main server MUC service has not been checked yet")
if muc_service is None:
warning(_("No MUC service found on main server"))
raise exceptions.FeatureNotFound
muc_service = muc_service.userhost()
return jid.JID("%s@%s" % (room_name, muc_service))
def join(self, room_jid, nick, options, profile_key=C.PROF_KEY_NONE):
def _errDeferred(exc_obj=Exception, txt='Error while joining room'):
@ -315,7 +331,7 @@ class XEP_0045(object):
warning(mess)
self.host.bridge.newAlert(mess, _("Group chat error"), "ERROR", profile)
return
d = self.join(room_jid, nick, options, profile)
self.join(room_jid, nick, options, profile)
# TODO: error management + signal in bridge
return room_jid_s

View file

@ -205,7 +205,7 @@ class XEP_0050(object):
'FORBIDDEN', 'ITEM_NOT_FOUND', 'FEATURE_NOT_IMPLEMENTED', 'INTERNAL'))(('bad-request', 'malformed-action'),
('bad-request', 'bad-action'), ('bad-request', 'bad-locale'), ('bad-request','bad-payload'),
('bad-request','bad-sessionid'), ('not-allowed','session-expired'), ('forbidden', None),
('item-not-found', None), ('feature-not-implemented', None), ('internal-server-error', None)) # XEP-0050 §4.6 Table 5
('item-not-found', None), ('feature-not-implemented', None), ('internal-server-error', None)) # XEP-0050 §4.4 Table 5
def __init__(self, host):
info(_("plugin XEP-0050 initialization"))

View file

@ -787,21 +787,23 @@ class XEP_0065_handler(XMPPHandler):
"File Transfer", profile_key=self.parent.profile)
def connectionInitialized(self):
def after_init(ignore):
proxy_ent = self.host.memory.getServerServiceEntity("proxy", "bytestreams", profile=self.parent.profile)
if not proxy_ent:
debug(_("No proxy found on this server"))
return
iq_elt = jabber_client.IQ(self.parent.xmlstream, 'get')
iq_elt["to"] = proxy_ent.full()
iq_elt.addElement('query', NS_BS)
iq_elt.addCallback(self._proxyDataResult)
iq_elt.send()
self.xmlstream.addObserver(BS_REQUEST, self.plugin_parent.streamQuery, profile=self.parent.profile)
proxy = self.host.memory.getParamA("Proxy", "File Transfer", profile_key=self.parent.profile)
if not proxy:
self.parent.client_initialized.addCallback(after_init)
def proxiesFound(entities):
try:
proxy_ent = entities.pop()
except KeyError:
info(_("No proxy found on this server"))
return
iq_elt = jabber_client.IQ(self.parent.xmlstream, 'get')
iq_elt["to"] = proxy_ent.full()
iq_elt.addElement('query', NS_BS)
iq_elt.addCallback(self._proxyDataResult)
iq_elt.send()
d = self.host.findServiceEntities("proxy", "bytestreams", profile_key=self.parent.profile)
d.addCallback(proxiesFound)
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
return [disco.DiscoFeature(NS_BS)]

View file

@ -22,16 +22,10 @@ from sat.core.constants import Const as C
from logging import debug, info, error, warning
from twisted.words.xish import domish
from twisted.words.protocols.jabber import jid
from sat.memory.persistent import PersistentBinaryDict
import types
from twisted.internet import defer
from zope.interface import implements
from wokkel import disco, iwokkel
from hashlib import sha1
from base64 import b64encode
try:
from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
@ -53,97 +47,44 @@ PLUGIN_INFO = {
}
class HashGenerationError(Exception):
pass
class ByteIdentity(object):
"""This class manage identity as bytes (needed for i;octet sort),
it is used for the hash generation"""
def __init__(self, identity, lang=None):
assert isinstance(identity, disco.DiscoIdentity)
self.category = identity.category.encode('utf-8')
self.idType = identity.type.encode('utf-8')
self.name = identity.name.encode('utf-8') if identity.name else ''
self.lang = lang.encode('utf-8') if lang else ''
def __str__(self):
return "%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name)
class XEP_0115(object):
cap_hash = None # capabilities hash is class variable as it is common to all profiles
#TODO: this code is really dirty, need to clean it and try to move it to Wokkel
def __init__(self, host):
info(_("Plugin XEP_0115 initialization"))
self.host = host
host.trigger.add("Disco Handled", self.checkHash)
self.hash_cache = PersistentBinaryDict(NS_ENTITY_CAPABILITY) # key = hash or jid, value = features
self.hash_cache.load()
self.jid_hash = {} # jid to hash mapping, map to a discoInfo features if the hash method is unknown
def checkHash(self, profile):
if not XEP_0115.cap_hash:
XEP_0115.cap_hash = self.generateHash(profile)
else:
self.presenceHack(profile)
return True
host.trigger.add("Disco handled", self._checkHash)
host.trigger.add("Presence send", self._presenceTrigger)
def getHandler(self, profile):
return XEP_0115_handler(self, profile)
def presenceHack(self, profile):
"""modify SatPresenceProtocol to add capabilities data"""
def _checkHash(self, disco_d, profile):
if XEP_0115.cap_hash is None:
disco_d.addCallback(lambda dummy: self.recalculateHash(profile))
return True
def _presenceTrigger(self, obj):
if XEP_0115.cap_hash is not None:
obj.addChild(XEP_0115.c_elt)
return True
@defer.inlineCallbacks
def recalculateHash(self, profile):
client = self.host.getClient(profile)
presenceInst = client.presence
disco_infos = yield client.discoHandler.info(client.jid, client.jid, '')
cap_hash = self.host.memory.disco.generateHash(disco_infos)
info("Our capability hash has been generated: [%s]" % cap_hash)
debug("Generating capability domish.Element")
c_elt = domish.Element((NS_ENTITY_CAPABILITY, 'c'))
c_elt['hash'] = 'sha-1'
c_elt['node'] = 'http://sat.goffi.org'
c_elt['ver'] = XEP_0115.cap_hash
presenceInst._c_elt = c_elt
if "_legacy_send" in dir(presenceInst):
debug('capabilities already added to presence instance')
return
def hacked_send(self, obj):
obj.addChild(self._c_elt)
self._legacy_send(obj)
new_send = types.MethodType(hacked_send, presenceInst, presenceInst.__class__)
presenceInst._legacy_send = presenceInst.send
presenceInst.send = new_send
def generateHash(self, profile_key=C.PROF_KEY_NONE):
"""This method generate a sha1 hash as explained in xep-0115 #5.1
it then store it in XEP_0115.cap_hash"""
profile = self.host.memory.getProfileName(profile_key)
if not profile:
error('Requesting hash for an inexistant profile')
raise HashGenerationError
client = self.host.getClient(profile_key)
def generateHash_2(services, profile):
_s = []
byte_identities = [ByteIdentity(service) for service in services if isinstance(service, disco.DiscoIdentity)] # FIXME: lang must be managed here
byte_identities.sort(key=lambda i: i.lang)
byte_identities.sort(key=lambda i: i.idType)
byte_identities.sort(key=lambda i: i.category)
for identity in byte_identities:
_s.append(str(identity))
_s.append('<')
byte_features = [service.encode('utf-8') for service in services if isinstance(service, disco.DiscoFeature)]
byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort
for feature in byte_features:
_s.append(feature)
_s.append('<')
#TODO: manage XEP-0128 data form here
XEP_0115.cap_hash = b64encode(sha1(''.join(_s)).digest())
debug(_('Capability hash generated: [%s]') % XEP_0115.cap_hash)
self.presenceHack(profile)
client.discoHandler.info(client.jid, client.jid, '').addCallback(generateHash_2, profile)
c_elt['node'] = C.APP_URL
c_elt['ver'] = cap_hash
XEP_0115.cap_hash = cap_hash
XEP_0115.c_elt = c_elt
if cap_hash not in self.host.memory.disco.hashes:
self.host.memory.disco.hashes[cap_hash] = disco_infos
self.host.memory.updateEntityData(client.jid, C.ENTITY_CAP_HASH, cap_hash, profile)
class XEP_0115_handler(XMPPHandler):
@ -163,38 +104,35 @@ class XEP_0115_handler(XMPPHandler):
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
return []
def _updateCache(self, discoResult, from_jid, key):
"""Actually update the cache
@param discoResult: result of the requestInfo"""
if key:
self.plugin_parent.jid_hash[from_jid] = key
self.plugin_parent.hash_cache[key] = discoResult.features
else:
#No key, that means unknown hash method
self.plugin_parent.jid_hash[from_jid] = discoResult.features
@defer.inlineCallbacks
def update(self, presence):
"""
Manage the capabilities of the entity
Check if we know the version of this capatilities
and get the capibilities if necessary
Check if we know the version of this capatilities and get the capibilities if necessary
"""
from_jid = jid.JID(presence['from'])
c_elem = filter(lambda x: x.name == "c", presence.elements())[0] # We only want the "c" element
c_elem = presence.elements(NS_ENTITY_CAPABILITY, 'c').next()
try:
ver = c_elem['ver']
hash = c_elem['hash']
node = c_elem['node']
c_ver = c_elem['ver']
c_hash = c_elem['hash']
c_node = c_elem['node']
except KeyError:
warning('Received invalid capabilities tag')
warning(_('Received invalid capabilities tag'))
return
if not from_jid in self.plugin_parent.jid_hash:
if ver in self.plugin_parent.hash_cache:
#we know that hash, we just link it with the jid
self.plugin_parent.jid_hash[from_jid] = ver
else:
if hash != 'sha-1':
#unknown hash method
warning('Unknown hash for entity capabilities: [%s]' % hash)
self.parent.disco.requestInfo(from_jid).addCallback(self._updateCache, from_jid, ver if hash == 'sha-1' else None)
#TODO: me must manage the full algorithm described at XEP-0115 #5.4 part 3
if c_ver in self.host.memory.disco.hashes:
# we already know the hash, we update the jid entity
debug ("hash [%s] already in cache, updating entity" % c_ver)
self.host.memory.updateEntityData(from_jid, C.ENTITY_CAP_HASH, c_ver, self.profile)
return
yield self.host.getDiscoInfos(from_jid, self.profile)
if c_hash != 'sha-1':
#unknown hash method
warning(_('Unknown hash method for entity capabilities: [%(hash_method)s] (entity: %(jid)s, node: %(node)s)') % {'hash_method':c_hash, 'jid': from_jid, 'node': c_node})
computed_hash = self.host.memory.getEntityDatum(from_jid, C.ENTITY_CAP_HASH, self.profile)
if computed_hash != c_ver:
warning(_('Computed hash differ from given hash:\ngiven: [%(given_hash)s]\ncomputed: [%(computed_hash)s]\n(entity: %(jid)s, node: %(node)s)') % {'given_hash':c_ver, 'computed_hash': computed_hash, 'jid': from_jid, 'node': c_node})
# TODO: me must manage the full algorithm described at XEP-0115 #5.4 part 3

View file

@ -90,26 +90,6 @@ class FakeSAT(object):
self.stored_messages.append(mess_data["to"])
pass
def requestServerDisco(self, feature, jid_=None, cache_only=False, profile_key="@NONE"):
"""Discover if a server or its items offer a given feature
@param feature: the feature to check
@param jid_: the jid of the server, local server if None
@param cache_only: expect the result to be in cache and don't actually
make any request. This can be used anytime for requesting a feature on
the local server because the data are cached for sure.
@result: the Deferred entity jid offering the feature, or None
"""
profile = self.memory.getProfileName(profile_key)
self.memory.server_features.setdefault(profile, {})
if jid_ is None:
jid_ = self.getClientHostJid(profile_key)
# call FakeMemory.init and FakeMemory.addServerFeature
# in your tests to change the return value of this method
return defer.succeed(jid_ if self.memory.hasServerFeature(feature, jid_, profile_key) else None)
def getClientHostJid(self, profile_key):
return Const.PROFILE_DICT[profile_key].host
def getClient(self, profile_key):
"""Convenient method to get client from profile key
@return: client or None if it doesn't exist"""
@ -118,7 +98,6 @@ class FakeSAT(object):
raise exceptions.ProfileKeyUnknownError
if profile not in self.profiles:
self.profiles[profile] = FakeClient(self, profile)
self.profiles[profile].client_initialized.callback(None)
return self.profiles[profile]
def getJidNStream(self, profile_key):
@ -241,7 +220,6 @@ class FakeMemory(Memory):
self.params.params.clear()
self.params.frontends_cache = []
self.entities_data = {}
self.server_features = {}
def getProfileName(self, profile_key, return_profile_keys=False):
return self.params.getProfileName(profile_key, return_profile_keys)
@ -323,7 +301,6 @@ class FakeClient(object):
self.profile = profile if profile else Const.PROFILE[0]
self.jid = Const.PROFILE_DICT[self.profile]
self.roster = FakeRosterProtocol(host, self)
self.client_initialized = defer.Deferred()
self.xmlstream = FakeXmlStream()
def send(self, obj):

View file

@ -20,9 +20,7 @@
from sat.test import helpers
from constants import Const
from twisted.trial import unittest
from sat.core.sat_main import SAT
from sat.core import xmpp
from twisted.internet import defer
from twisted.words.protocols.jabber.jid import JID
from wokkel.generic import parseXml
from wokkel.xmppim import RosterItem
@ -39,7 +37,6 @@ class SatXMPPClientTest(unittest.TestCase):
self.assertEqual(self.client.profile, Const.PROFILE[0])
print self.client.host
self.assertEqual(self.client.host_app, self.host)
self.assertTrue(isinstance(self.client.client_initialized, defer.Deferred))
class SatMessageProtocolTest(unittest.TestCase):

View file

@ -32,6 +32,7 @@ import logging
class XEP_0033Test(helpers.SatTestCase):
skip = "Must be fixed after disco changes"
def setUp(self):
self.host = helpers.FakeSAT()
@ -151,7 +152,6 @@ class XEP_0033Test(helpers.SatTestCase):
# feature is supported
self.host.init()
self.host.memory.init()
self.host.memory.addServerFeature(plugin.NS_ADDRESS, self.host.getClientHostJid(Const.PROFILE[0]), Const.PROFILE[0])
data = deepcopy(mess_data)
trigger(data, MessageSentAndStored)
checkSentAndStored()
@ -159,7 +159,6 @@ class XEP_0033Test(helpers.SatTestCase):
# check that a wrong recipient entity is fixed by the backend
self.host.init()
self.host.memory.init()
self.host.memory.addServerFeature(plugin.NS_ADDRESS, self.host.getClientHostJid(Const.PROFILE[0]), Const.PROFILE[0])
data = deepcopy(mess_data)
data["to"] = Const.JID[0]
trigger(data, MessageSentAndStored)