Mercurial > libervia-backend
diff src/memory/disco.py @ 944:e1842ebcb2f3
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
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 28 Mar 2014 18:07:22 +0100 |
parents | src/plugins/plugin_xep_0115.py@c6d8fc63b1db |
children | 027a054c6dda |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/memory/disco.py Fri Mar 28 18:07:22 2014 +0100 @@ -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