Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0115.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 | c6d8fc63b1db |
children | 4a577b170809 |
comparison
equal
deleted
inserted
replaced
943:71926ec2114d | 944:e1842ebcb2f3 |
---|---|
20 from sat.core.i18n import _ | 20 from sat.core.i18n import _ |
21 from sat.core.constants import Const as C | 21 from sat.core.constants import Const as C |
22 from logging import debug, info, error, warning | 22 from logging import debug, info, error, warning |
23 from twisted.words.xish import domish | 23 from twisted.words.xish import domish |
24 from twisted.words.protocols.jabber import jid | 24 from twisted.words.protocols.jabber import jid |
25 from sat.memory.persistent import PersistentBinaryDict | 25 from twisted.internet import defer |
26 import types | |
27 | |
28 from zope.interface import implements | 26 from zope.interface import implements |
29 | |
30 from wokkel import disco, iwokkel | 27 from wokkel import disco, iwokkel |
31 | |
32 from hashlib import sha1 | |
33 from base64 import b64encode | |
34 | 28 |
35 try: | 29 try: |
36 from twisted.words.protocols.xmlstream import XMPPHandler | 30 from twisted.words.protocols.xmlstream import XMPPHandler |
37 except ImportError: | 31 except ImportError: |
38 from wokkel.subprotocols import XMPPHandler | 32 from wokkel.subprotocols import XMPPHandler |
51 "handler": "yes", | 45 "handler": "yes", |
52 "description": _("""Implementation of entity capabilities""") | 46 "description": _("""Implementation of entity capabilities""") |
53 } | 47 } |
54 | 48 |
55 | 49 |
56 class HashGenerationError(Exception): | |
57 pass | |
58 | |
59 | |
60 class ByteIdentity(object): | |
61 """This class manage identity as bytes (needed for i;octet sort), | |
62 it is used for the hash generation""" | |
63 | |
64 def __init__(self, identity, lang=None): | |
65 assert isinstance(identity, disco.DiscoIdentity) | |
66 self.category = identity.category.encode('utf-8') | |
67 self.idType = identity.type.encode('utf-8') | |
68 self.name = identity.name.encode('utf-8') if identity.name else '' | |
69 self.lang = lang.encode('utf-8') if lang else '' | |
70 | |
71 def __str__(self): | |
72 return "%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name) | |
73 | |
74 | |
75 class XEP_0115(object): | 50 class XEP_0115(object): |
76 cap_hash = None # capabilities hash is class variable as it is common to all profiles | 51 cap_hash = None # capabilities hash is class variable as it is common to all profiles |
77 #TODO: this code is really dirty, need to clean it and try to move it to Wokkel | |
78 | 52 |
79 def __init__(self, host): | 53 def __init__(self, host): |
80 info(_("Plugin XEP_0115 initialization")) | 54 info(_("Plugin XEP_0115 initialization")) |
81 self.host = host | 55 self.host = host |
82 host.trigger.add("Disco Handled", self.checkHash) | 56 host.trigger.add("Disco handled", self._checkHash) |
83 self.hash_cache = PersistentBinaryDict(NS_ENTITY_CAPABILITY) # key = hash or jid, value = features | 57 host.trigger.add("Presence send", self._presenceTrigger) |
84 self.hash_cache.load() | |
85 self.jid_hash = {} # jid to hash mapping, map to a discoInfo features if the hash method is unknown | |
86 | |
87 def checkHash(self, profile): | |
88 if not XEP_0115.cap_hash: | |
89 XEP_0115.cap_hash = self.generateHash(profile) | |
90 else: | |
91 self.presenceHack(profile) | |
92 return True | |
93 | 58 |
94 def getHandler(self, profile): | 59 def getHandler(self, profile): |
95 return XEP_0115_handler(self, profile) | 60 return XEP_0115_handler(self, profile) |
96 | 61 |
97 def presenceHack(self, profile): | 62 def _checkHash(self, disco_d, profile): |
98 """modify SatPresenceProtocol to add capabilities data""" | 63 if XEP_0115.cap_hash is None: |
64 disco_d.addCallback(lambda dummy: self.recalculateHash(profile)) | |
65 return True | |
66 | |
67 def _presenceTrigger(self, obj): | |
68 if XEP_0115.cap_hash is not None: | |
69 obj.addChild(XEP_0115.c_elt) | |
70 return True | |
71 | |
72 @defer.inlineCallbacks | |
73 def recalculateHash(self, profile): | |
99 client = self.host.getClient(profile) | 74 client = self.host.getClient(profile) |
100 presenceInst = client.presence | 75 disco_infos = yield client.discoHandler.info(client.jid, client.jid, '') |
76 cap_hash = self.host.memory.disco.generateHash(disco_infos) | |
77 info("Our capability hash has been generated: [%s]" % cap_hash) | |
78 debug("Generating capability domish.Element") | |
101 c_elt = domish.Element((NS_ENTITY_CAPABILITY, 'c')) | 79 c_elt = domish.Element((NS_ENTITY_CAPABILITY, 'c')) |
102 c_elt['hash'] = 'sha-1' | 80 c_elt['hash'] = 'sha-1' |
103 c_elt['node'] = 'http://sat.goffi.org' | 81 c_elt['node'] = C.APP_URL |
104 c_elt['ver'] = XEP_0115.cap_hash | 82 c_elt['ver'] = cap_hash |
105 presenceInst._c_elt = c_elt | 83 XEP_0115.cap_hash = cap_hash |
106 if "_legacy_send" in dir(presenceInst): | 84 XEP_0115.c_elt = c_elt |
107 debug('capabilities already added to presence instance') | 85 if cap_hash not in self.host.memory.disco.hashes: |
108 return | 86 self.host.memory.disco.hashes[cap_hash] = disco_infos |
109 | 87 self.host.memory.updateEntityData(client.jid, C.ENTITY_CAP_HASH, cap_hash, profile) |
110 def hacked_send(self, obj): | |
111 obj.addChild(self._c_elt) | |
112 self._legacy_send(obj) | |
113 new_send = types.MethodType(hacked_send, presenceInst, presenceInst.__class__) | |
114 presenceInst._legacy_send = presenceInst.send | |
115 presenceInst.send = new_send | |
116 | |
117 def generateHash(self, profile_key=C.PROF_KEY_NONE): | |
118 """This method generate a sha1 hash as explained in xep-0115 #5.1 | |
119 it then store it in XEP_0115.cap_hash""" | |
120 profile = self.host.memory.getProfileName(profile_key) | |
121 if not profile: | |
122 error('Requesting hash for an inexistant profile') | |
123 raise HashGenerationError | |
124 | |
125 client = self.host.getClient(profile_key) | |
126 | |
127 def generateHash_2(services, profile): | |
128 _s = [] | |
129 byte_identities = [ByteIdentity(service) for service in services if isinstance(service, disco.DiscoIdentity)] # FIXME: lang must be managed here | |
130 byte_identities.sort(key=lambda i: i.lang) | |
131 byte_identities.sort(key=lambda i: i.idType) | |
132 byte_identities.sort(key=lambda i: i.category) | |
133 for identity in byte_identities: | |
134 _s.append(str(identity)) | |
135 _s.append('<') | |
136 byte_features = [service.encode('utf-8') for service in services if isinstance(service, disco.DiscoFeature)] | |
137 byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort | |
138 for feature in byte_features: | |
139 _s.append(feature) | |
140 _s.append('<') | |
141 #TODO: manage XEP-0128 data form here | |
142 XEP_0115.cap_hash = b64encode(sha1(''.join(_s)).digest()) | |
143 debug(_('Capability hash generated: [%s]') % XEP_0115.cap_hash) | |
144 self.presenceHack(profile) | |
145 | |
146 client.discoHandler.info(client.jid, client.jid, '').addCallback(generateHash_2, profile) | |
147 | 88 |
148 | 89 |
149 class XEP_0115_handler(XMPPHandler): | 90 class XEP_0115_handler(XMPPHandler): |
150 implements(iwokkel.IDisco) | 91 implements(iwokkel.IDisco) |
151 | 92 |
161 return [disco.DiscoFeature(NS_ENTITY_CAPABILITY)] | 102 return [disco.DiscoFeature(NS_ENTITY_CAPABILITY)] |
162 | 103 |
163 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | 104 def getDiscoItems(self, requestor, target, nodeIdentifier=''): |
164 return [] | 105 return [] |
165 | 106 |
166 def _updateCache(self, discoResult, from_jid, key): | 107 @defer.inlineCallbacks |
167 """Actually update the cache | |
168 @param discoResult: result of the requestInfo""" | |
169 if key: | |
170 self.plugin_parent.jid_hash[from_jid] = key | |
171 self.plugin_parent.hash_cache[key] = discoResult.features | |
172 else: | |
173 #No key, that means unknown hash method | |
174 self.plugin_parent.jid_hash[from_jid] = discoResult.features | |
175 | |
176 def update(self, presence): | 108 def update(self, presence): |
177 """ | 109 """ |
178 Manage the capabilities of the entity | 110 Manage the capabilities of the entity |
179 Check if we know the version of this capatilities | 111 |
180 and get the capibilities if necessary | 112 Check if we know the version of this capatilities and get the capibilities if necessary |
181 """ | 113 """ |
182 from_jid = jid.JID(presence['from']) | 114 from_jid = jid.JID(presence['from']) |
183 c_elem = filter(lambda x: x.name == "c", presence.elements())[0] # We only want the "c" element | 115 c_elem = presence.elements(NS_ENTITY_CAPABILITY, 'c').next() |
184 try: | 116 try: |
185 ver = c_elem['ver'] | 117 c_ver = c_elem['ver'] |
186 hash = c_elem['hash'] | 118 c_hash = c_elem['hash'] |
187 node = c_elem['node'] | 119 c_node = c_elem['node'] |
188 except KeyError: | 120 except KeyError: |
189 warning('Received invalid capabilities tag') | 121 warning(_('Received invalid capabilities tag')) |
190 return | 122 return |
191 if not from_jid in self.plugin_parent.jid_hash: | 123 |
192 if ver in self.plugin_parent.hash_cache: | 124 if c_ver in self.host.memory.disco.hashes: |
193 #we know that hash, we just link it with the jid | 125 # we already know the hash, we update the jid entity |
194 self.plugin_parent.jid_hash[from_jid] = ver | 126 debug ("hash [%s] already in cache, updating entity" % c_ver) |
195 else: | 127 self.host.memory.updateEntityData(from_jid, C.ENTITY_CAP_HASH, c_ver, self.profile) |
196 if hash != 'sha-1': | 128 return |
197 #unknown hash method | 129 |
198 warning('Unknown hash for entity capabilities: [%s]' % hash) | 130 yield self.host.getDiscoInfos(from_jid, self.profile) |
199 self.parent.disco.requestInfo(from_jid).addCallback(self._updateCache, from_jid, ver if hash == 'sha-1' else None) | 131 if c_hash != 'sha-1': |
200 #TODO: me must manage the full algorithm described at XEP-0115 #5.4 part 3 | 132 #unknown hash method |
133 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}) | |
134 computed_hash = self.host.memory.getEntityDatum(from_jid, C.ENTITY_CAP_HASH, self.profile) | |
135 if computed_hash != c_ver: | |
136 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}) | |
137 | |
138 # TODO: me must manage the full algorithm described at XEP-0115 #5.4 part 3 |