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