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