comparison 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
comparison
equal deleted inserted replaced
943:71926ec2114d 944:e1842ebcb2f3
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # SAT: a jabber client
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from logging import debug, info, warning, error
22 from twisted.words.protocols.jabber import jid
23 from twisted.internet import defer
24 from sat.core.constants import Const as C
25 from wokkel import disco
26 from base64 import b64encode
27 from hashlib import sha1
28
29
30 PRESENCE = '/presence'
31 NS_ENTITY_CAPABILITY = 'http://jabber.org/protocol/caps'
32 CAPABILITY_UPDATE = PRESENCE + '/c[@xmlns="' + NS_ENTITY_CAPABILITY + '"]'
33
34 class HashGenerationError(Exception):
35 pass
36
37
38 class ByteIdentity(object):
39 """This class manage identity as bytes (needed for i;octet sort), it is used for the hash generation"""
40
41 def __init__(self, identity, lang=None):
42 assert isinstance(identity, disco.DiscoIdentity)
43 self.category = identity.category.encode('utf-8')
44 self.idType = identity.type.encode('utf-8')
45 self.name = identity.name.encode('utf-8') if identity.name else ''
46 self.lang = lang.encode('utf-8') if lang is not None else ''
47
48 def __str__(self):
49 return "%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name)
50
51
52 class Discovery(object):
53 """ Manage capabilities of entities """
54
55 def __init__(self, host):
56 self.host = host
57 self.hashes = {} # key: capability hash, value: disco features/identities
58 # TODO: save hashes in databse, remove legacy hashes
59
60 @defer.inlineCallbacks
61 def hasFeature(self, feature, jid_=None, profile_key=C.PROF_KEY_NONE):
62 """Tell if an entity has the required feature
63
64 @param feature: feature namespace
65 @param jid_: jid of the target, or None for profile's server
66 @param profile_key: %(doc_profile_key)s
67 @return: a Deferred which fire a boolean (True if feature is available)
68 """
69 disco_info = yield self.getInfos(jid_, profile_key)
70 defer.returnValue(feature in disco_info)
71
72 @defer.inlineCallbacks
73 def getInfos(self, jid_=None, profile_key=C.PROF_KEY_NONE):
74 """get disco infos from jid_, filling capability hash if needed
75
76 @param jid_: jid of the target, or None for profile's server
77 @param profile_key: %(doc_profile_key)s
78 @return: a Deferred which fire disco.DiscoInfo
79 """
80 client = self.host.getClient(profile_key)
81 if jid_ is None:
82 jid_ = jid.JID(client.jid.host)
83 try:
84 cap_hash = self.host.memory.getEntityData(jid_, [C.ENTITY_CAP_HASH], client.profile)[C.ENTITY_CAP_HASH]
85 disco_info = self.hashes[cap_hash]
86 except KeyError:
87 # capability hash is not available, we'll compute one
88 disco_info = yield client.disco.requestInfo(jid_)
89 cap_hash = self.generateHash(disco_info)
90 self.hashes[cap_hash] = disco_info
91 yield self.host.memory.updateEntityData(jid_, C.ENTITY_CAP_HASH, cap_hash, client.profile)
92 defer.returnValue(disco_info)
93
94 @defer.inlineCallbacks
95 def getItems(self, jid_=None, profile_key=C.PROF_KEY_NONE):
96 """get disco items from jid_, cache them for our own server
97
98 @param jid_: jid of the target, or None for profile's server
99 @param profile_key: %(doc_profile_key)s
100 @return: a Deferred which fire disco.DiscoInfo
101 """
102 client = self.host.getClient(profile_key)
103 if jid_ is None:
104 jid_ = jid.JID(client.jid.host)
105 # we cache items only for our own server
106 try:
107 items = self.host.memory.getEntityData(jid_, ["DISCO_ITEMS"], client.profile)["DISCO_ITEMS"]
108 debug("[%s] disco items are in cache" % jid_.full())
109 except KeyError:
110 debug("Caching [%s] disco items" % jid_.full())
111 items = yield client.disco.requestItems(jid_)
112 self.host.memory.updateEntityData(jid_, "DISCO_ITEMS", items, client.profile)
113 else:
114 items = yield client.disco.requestItems(jid_)
115
116 defer.returnValue(items)
117
118
119 @defer.inlineCallbacks
120 def findServiceEntities(self, category, type_, jid_=None, profile_key=C.PROF_KEY_NONE):
121 """Return all available items of an entity which correspond to (category, type_)
122
123 @param category: identity's category
124 @param type_: identitiy's type
125 @param jid_: the jid of the target server (None for profile's server)
126 @param profile_key: %(doc_profile_key)s
127 @return: a set of entities or None if no cached data were found
128 """
129 found_identities = set()
130 items = yield self.getItems(jid_, profile_key)
131 for item in items:
132 infos = yield self.getInfos(item.entity, profile_key)
133 if (category, type_) in infos.identities:
134 found_identities.add(item.entity)
135
136 defer.returnValue(found_identities)
137
138 @defer.inlineCallbacks
139 def findFeaturesSet(self, features, category=None, type_=None, jid_=None, profile_key=C.PROF_KEY_NONE):
140 """Return entities (including jid_ and its items) offering features
141
142 @param features: iterable of features which must be present
143 @param category: if not None, accept only this category
144 @param type_: if not None, accept only this type
145 @param jid_: the jid of the target server (None for profile's server)
146 @param profile_key: %(doc_profile_key)s
147 @return: a set of found entities
148 """
149 client = self.host.getClient(profile_key)
150 if jid_ is None:
151 jid_ = jid.JID(client.jid.host)
152 features = set(features)
153 found_entities = set()
154
155 items = yield self.getItems(jid_, profile_key)
156 for entity in [jid_] + items:
157 infos = yield self.getInfos(entity, profile_key)
158 if category is not None or type_ is not None:
159 categories = set()
160 types = set()
161 for identity in infos.identities:
162 id_cat, id_type = identity
163 categories.add(id_cat)
164 types.add(id_type)
165 if category is not None and category not in categories:
166 continue
167 if type_ is not None and type_ not in types:
168 continue
169 if features.issubset(infos.features):
170 found_entities.add(entity)
171
172 defer.returnValue(found_entities)
173
174 def generateHash(self, services):
175 """ Generate a unique hash for given service
176
177 hash algorithm is the one described in XEP-0115
178 @param services: iterable of disco.DiscoIdentity/disco.DiscoFeature, as returned by discoHandler.info
179
180 """
181 s = []
182 byte_identities = [ByteIdentity(service) for service in services if isinstance(service, disco.DiscoIdentity)] # FIXME: lang must be managed here
183 byte_identities.sort(key=lambda i: i.lang)
184 byte_identities.sort(key=lambda i: i.idType)
185 byte_identities.sort(key=lambda i: i.category)
186 for identity in byte_identities:
187 s.append(str(identity))
188 s.append('<')
189 byte_features = [service.encode('utf-8') for service in services if isinstance(service, disco.DiscoFeature)]
190 byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort
191 for feature in byte_features:
192 s.append(feature)
193 s.append('<')
194 #TODO: manage XEP-0128 data form here
195 cap_hash = b64encode(sha1(''.join(s)).digest())
196 debug(_('Capability hash generated: [%s]') % cap_hash)
197 return cap_hash