comparison sat/memory/disco.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents b6abf8af87db
children fee60f17ebac
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
1 #!/usr/bin/env python2 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # SAT: a jabber client 4 # SAT: a jabber client
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6 6
48 48
49 def __init__(self, identity, lang=None): 49 def __init__(self, identity, lang=None):
50 assert isinstance(identity, disco.DiscoIdentity) 50 assert isinstance(identity, disco.DiscoIdentity)
51 self.category = identity.category.encode("utf-8") 51 self.category = identity.category.encode("utf-8")
52 self.idType = identity.type.encode("utf-8") 52 self.idType = identity.type.encode("utf-8")
53 self.name = identity.name.encode("utf-8") if identity.name else "" 53 self.name = identity.name.encode("utf-8") if identity.name else b""
54 self.lang = lang.encode("utf-8") if lang is not None else "" 54 self.lang = lang.encode("utf-8") if lang is not None else b""
55 55
56 def __str__(self): 56 def __bytes__(self):
57 return "%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name) 57 return b"%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name)
58 58
59 59
60 class HashManager(object): 60 class HashManager(object):
61 """map object which manage hashes 61 """map object which manage hashes
62 62
72 def __getitem__(self, key): 72 def __getitem__(self, key):
73 return self.hashes[key] 73 return self.hashes[key]
74 74
75 def __setitem__(self, hash_, disco_info): 75 def __setitem__(self, hash_, disco_info):
76 if hash_ in self.hashes: 76 if hash_ in self.hashes:
77 log.debug(u"ignoring hash set: it is already known") 77 log.debug("ignoring hash set: it is already known")
78 return 78 return
79 self.hashes[hash_] = disco_info 79 self.hashes[hash_] = disco_info
80 self.persistent[hash_] = disco_info.toElement().toXml() 80 self.persistent[hash_] = disco_info.toElement().toXml()
81 81
82 def __contains__(self, hash_): 82 def __contains__(self, hash_):
83 return self.hashes.__contains__(hash_) 83 return self.hashes.__contains__(hash_)
84 84
85 def load(self): 85 def load(self):
86 def fillHashes(hashes): 86 def fillHashes(hashes):
87 for hash_, xml in hashes.iteritems(): 87 for hash_, xml in hashes.items():
88 element = xml_tools.ElementParser()(xml) 88 element = xml_tools.ElementParser()(xml)
89 disco_info = disco.DiscoInfo.fromElement(element) 89 disco_info = disco.DiscoInfo.fromElement(element)
90 if not disco_info.features and not disco_info.identities: 90 if not disco_info.features and not disco_info.identities:
91 log.warning( 91 log.warning(
92 _( 92 _(
93 u"no feature/identity found in disco element (hash: {cap_hash}), ignoring: {xml}" 93 "no feature/identity found in disco element (hash: {cap_hash}), ignoring: {xml}"
94 ).format(cap_hash=hash_, xml=xml) 94 ).format(cap_hash=hash_, xml=xml)
95 ) 95 )
96 else: 96 else:
97 self.hashes[hash_] = disco_info 97 self.hashes[hash_] = disco_info
98 98
99 log.info(u"Disco hashes loaded") 99 log.info("Disco hashes loaded")
100 100
101 d = self.persistent.load() 101 d = self.persistent.load()
102 d.addCallback(fillHashes) 102 d.addCallback(fillHashes)
103 return d 103 return d
104 104
114 """Load persistent hashes""" 114 """Load persistent hashes"""
115 self.hashes = HashManager(persistent.PersistentDict("disco")) 115 self.hashes = HashManager(persistent.PersistentDict("disco"))
116 return self.hashes.load() 116 return self.hashes.load()
117 117
118 @defer.inlineCallbacks 118 @defer.inlineCallbacks
119 def hasFeature(self, client, feature, jid_=None, node=u""): 119 def hasFeature(self, client, feature, jid_=None, node=""):
120 """Tell if an entity has the required feature 120 """Tell if an entity has the required feature
121 121
122 @param feature: feature namespace 122 @param feature: feature namespace
123 @param jid_: jid of the target, or None for profile's server 123 @param jid_: jid of the target, or None for profile's server
124 @param node(unicode): optional node to use for disco request 124 @param node(unicode): optional node to use for disco request
126 """ 126 """
127 disco_infos = yield self.getInfos(client, jid_, node) 127 disco_infos = yield self.getInfos(client, jid_, node)
128 defer.returnValue(feature in disco_infos.features) 128 defer.returnValue(feature in disco_infos.features)
129 129
130 @defer.inlineCallbacks 130 @defer.inlineCallbacks
131 def checkFeature(self, client, feature, jid_=None, node=u""): 131 def checkFeature(self, client, feature, jid_=None, node=""):
132 """Like hasFeature, but raise an exception is feature is not Found 132 """Like hasFeature, but raise an exception is feature is not Found
133 133
134 @param feature: feature namespace 134 @param feature: feature namespace
135 @param jid_: jid of the target, or None for profile's server 135 @param jid_: jid of the target, or None for profile's server
136 @param node(unicode): optional node to use for disco request 136 @param node(unicode): optional node to use for disco request
140 disco_infos = yield self.getInfos(client, jid_, node) 140 disco_infos = yield self.getInfos(client, jid_, node)
141 if not feature in disco_infos.features: 141 if not feature in disco_infos.features:
142 raise failure.Failure(exceptions.FeatureNotFound) 142 raise failure.Failure(exceptions.FeatureNotFound)
143 143
144 @defer.inlineCallbacks 144 @defer.inlineCallbacks
145 def checkFeatures(self, client, features, jid_=None, identity=None, node=u""): 145 def checkFeatures(self, client, features, jid_=None, identity=None, node=""):
146 """Like checkFeature, but check several features at once, and check also identity 146 """Like checkFeature, but check several features at once, and check also identity
147 147
148 @param features(iterable[unicode]): features to check 148 @param features(iterable[unicode]): features to check
149 @param jid_(jid.JID): jid of the target, or None for profile's server 149 @param jid_(jid.JID): jid of the target, or None for profile's server
150 @param node(unicode): optional node to use for disco request 150 @param node(unicode): optional node to use for disco request
157 raise failure.Failure(exceptions.FeatureNotFound()) 157 raise failure.Failure(exceptions.FeatureNotFound())
158 158
159 if identity is not None and identity not in disco_infos.identities: 159 if identity is not None and identity not in disco_infos.identities:
160 raise failure.Failure(exceptions.FeatureNotFound()) 160 raise failure.Failure(exceptions.FeatureNotFound())
161 161
162 def getInfos(self, client, jid_=None, node=u"", use_cache=True): 162 def getInfos(self, client, jid_=None, node="", use_cache=True):
163 """get disco infos from jid_, filling capability hash if needed 163 """get disco infos from jid_, filling capability hash if needed
164 164
165 @param jid_: jid of the target, or None for profile's server 165 @param jid_: jid of the target, or None for profile's server
166 @param node(unicode): optional node to use for disco request 166 @param node(unicode): optional node to use for disco request
167 @param use_cache(bool): if True, use cached data if available 167 @param use_cache(bool): if True, use cached data if available
186 ) 186 )
187 return disco_infos 187 return disco_infos
188 188
189 def infosEb(fail): 189 def infosEb(fail):
190 if fail.check(defer.CancelledError): 190 if fail.check(defer.CancelledError):
191 reason = u"request time-out" 191 reason = "request time-out"
192 fail = failure.Failure(exceptions.TimeOutError(fail.message)) 192 fail = failure.Failure(exceptions.TimeOutError(fail.message))
193 else: 193 else:
194 try: 194 try:
195 reason = unicode(fail.value) 195 reason = str(fail.value)
196 except AttributeError: 196 except AttributeError:
197 reason = unicode(fail) 197 reason = str(fail)
198 198
199 log.warning( 199 log.warning(
200 u"Error while requesting disco infos from {jid}: {reason}".format( 200 "Error while requesting disco infos from {jid}: {reason}".format(
201 jid=jid_.full(), reason=reason 201 jid=jid_.full(), reason=reason
202 ) 202 )
203 ) 203 )
204 204
205 # XXX we set empty disco in cache, to avoid getting an error or waiting 205 # XXX we set empty disco in cache, to avoid getting an error or waiting
216 else: 216 else:
217 disco_infos = self.hashes[cap_hash] 217 disco_infos = self.hashes[cap_hash]
218 return defer.succeed(disco_infos) 218 return defer.succeed(disco_infos)
219 219
220 @defer.inlineCallbacks 220 @defer.inlineCallbacks
221 def getItems(self, client, jid_=None, node=u"", use_cache=True): 221 def getItems(self, client, jid_=None, node="", use_cache=True):
222 """get disco items from jid_, cache them for our own server 222 """get disco items from jid_, cache them for our own server
223 223
224 @param jid_(jid.JID): jid of the target, or None for profile's server 224 @param jid_(jid.JID): jid of the target, or None for profile's server
225 @param node(unicode): optional node to use for disco request 225 @param node(unicode): optional node to use for disco request
226 @param use_cache(bool): if True, use cached data if available 226 @param use_cache(bool): if True, use cached data if available
234 # we cache items only for our own server and if node is not set 234 # we cache items only for our own server and if node is not set
235 try: 235 try:
236 items = self.host.memory.getEntityData( 236 items = self.host.memory.getEntityData(
237 jid_, ["DISCO_ITEMS"], client.profile 237 jid_, ["DISCO_ITEMS"], client.profile
238 )["DISCO_ITEMS"] 238 )["DISCO_ITEMS"]
239 log.debug(u"[%s] disco items are in cache" % jid_.full()) 239 log.debug("[%s] disco items are in cache" % jid_.full())
240 if not use_cache: 240 if not use_cache:
241 # we ignore cache, so we pretend we haven't found it 241 # we ignore cache, so we pretend we haven't found it
242 raise KeyError 242 raise KeyError
243 except (KeyError, exceptions.UnknownEntityError): 243 except (KeyError, exceptions.UnknownEntityError):
244 log.debug(u"Caching [%s] disco items" % jid_.full()) 244 log.debug("Caching [%s] disco items" % jid_.full())
245 items = yield client.disco.requestItems(jid_, nodeIdentifier=node) 245 items = yield client.disco.requestItems(jid_, nodeIdentifier=node)
246 self.host.memory.updateEntityData( 246 self.host.memory.updateEntityData(
247 jid_, "DISCO_ITEMS", items, profile_key=client.profile 247 jid_, "DISCO_ITEMS", items, profile_key=client.profile
248 ) 248 )
249 else: 249 else:
250 try: 250 try:
251 items = yield client.disco.requestItems(jid_, nodeIdentifier=node) 251 items = yield client.disco.requestItems(jid_, nodeIdentifier=node)
252 except StanzaError as e: 252 except StanzaError as e:
253 log.warning( 253 log.warning(
254 u"Error while requesting items for {jid}: {reason}".format( 254 "Error while requesting items for {jid}: {reason}".format(
255 jid=jid_.full(), reason=e.condition 255 jid=jid_.full(), reason=e.condition
256 ) 256 )
257 ) 257 )
258 items = disco.DiscoItems() 258 items = disco.DiscoItems()
259 259
260 defer.returnValue(items) 260 defer.returnValue(items)
261 261
262 def _infosEb(self, failure_, entity_jid): 262 def _infosEb(self, failure_, entity_jid):
263 failure_.trap(StanzaError) 263 failure_.trap(StanzaError)
264 log.warning( 264 log.warning(
265 _(u"Error while requesting [%(jid)s]: %(error)s") 265 _("Error while requesting [%(jid)s]: %(error)s")
266 % {"jid": entity_jid.full(), "error": failure_.getErrorMessage()} 266 % {"jid": entity_jid.full(), "error": failure_.getErrorMessage()}
267 ) 267 )
268 268
269 def findServiceEntity(self, client, category, type_, jid_=None): 269 def findServiceEntity(self, client, category, type_, jid_=None):
270 """Helper method to find first available entity from findServiceEntities 270 """Helper method to find first available entity from findServiceEntities
324 features = set(features) 324 features = set(features)
325 found_entities = set() 325 found_entities = set()
326 326
327 def infosCb(infos, entity): 327 def infosCb(infos, entity):
328 if entity is None: 328 if entity is None:
329 log.warning(_(u"received an item without jid")) 329 log.warning(_("received an item without jid"))
330 return 330 return
331 if identity is not None and identity not in infos.identities: 331 if identity is not None and identity not in infos.identities:
332 return 332 return
333 if features.issubset(infos.features): 333 if features.issubset(infos.features):
334 found_entities.add(entity) 334 found_entities.add(entity)
365 ] # FIXME: lang must be managed here 365 ] # FIXME: lang must be managed here
366 byte_identities.sort(key=lambda i: i.lang) 366 byte_identities.sort(key=lambda i: i.lang)
367 byte_identities.sort(key=lambda i: i.idType) 367 byte_identities.sort(key=lambda i: i.idType)
368 byte_identities.sort(key=lambda i: i.category) 368 byte_identities.sort(key=lambda i: i.category)
369 for identity in byte_identities: 369 for identity in byte_identities:
370 s.append(str(identity)) 370 s.append(bytes(identity))
371 s.append("<") 371 s.append(b"<")
372 # features 372 # features
373 byte_features = [ 373 byte_features = [
374 service.encode("utf-8") 374 service.encode("utf-8")
375 for service in services 375 for service in services
376 if isinstance(service, disco.DiscoFeature) 376 if isinstance(service, disco.DiscoFeature)
377 ] 377 ]
378 byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort 378 byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort
379 for feature in byte_features: 379 for feature in byte_features:
380 s.append(feature) 380 s.append(feature)
381 s.append("<") 381 s.append(b"<")
382 382
383 # extensions 383 # extensions
384 ext = services.extensions.values() 384 ext = list(services.extensions.values())
385 ext.sort(key=lambda f: f.formNamespace.encode('utf-8')) 385 ext.sort(key=lambda f: f.formNamespace.encode('utf-8'))
386 for extension in ext: 386 for extension in ext:
387 s.append(extension.formNamespace.encode('utf-8')) 387 s.append(extension.formNamespace.encode('utf-8'))
388 s.append("<") 388 s.append(b"<")
389 fields = extension.fieldList 389 fields = extension.fieldList
390 fields.sort(key=lambda f: f.var.encode('utf-8')) 390 fields.sort(key=lambda f: f.var.encode('utf-8'))
391 for field in fields: 391 for field in fields:
392 s.append(field.var.encode('utf-8')) 392 s.append(field.var.encode('utf-8'))
393 s.append("<") 393 s.append(b"<")
394 values = [v.encode('utf-8') for v in field.values] 394 values = [v.encode('utf-8') for v in field.values]
395 values.sort() 395 values.sort()
396 for value in values: 396 for value in values:
397 s.append(value) 397 s.append(value)
398 s.append("<") 398 s.append(b"<")
399 399
400 cap_hash = b64encode(sha1("".join(s)).digest()) 400 cap_hash = b64encode(sha1(b"".join(s)).digest()).decode('utf-8')
401 log.debug(_(u"Capability hash generated: [{cap_hash}]").format(cap_hash=cap_hash)) 401 log.debug(_("Capability hash generated: [{cap_hash}]").format(cap_hash=cap_hash))
402 return cap_hash 402 return cap_hash
403 403
404 @defer.inlineCallbacks 404 @defer.inlineCallbacks
405 def _discoInfos( 405 def _discoInfos(
406 self, entity_jid_s, node=u"", use_cache=True, profile_key=C.PROF_KEY_NONE 406 self, entity_jid_s, node="", use_cache=True, profile_key=C.PROF_KEY_NONE
407 ): 407 ):
408 """ Discovery method for the bridge 408 """ Discovery method for the bridge
409 @param entity_jid_s: entity we want to discover 409 @param entity_jid_s: entity we want to discover
410 @param use_cache(bool): if True, use cached data if available 410 @param use_cache(bool): if True, use cached data if available
411 @param node(unicode): optional node to use 411 @param node(unicode): optional node to use
415 client = self.host.getClient(profile_key) 415 client = self.host.getClient(profile_key)
416 entity = jid.JID(entity_jid_s) 416 entity = jid.JID(entity_jid_s)
417 disco_infos = yield self.getInfos(client, entity, node, use_cache) 417 disco_infos = yield self.getInfos(client, entity, node, use_cache)
418 extensions = {} 418 extensions = {}
419 # FIXME: should extensions be serialised using tools.common.data_format? 419 # FIXME: should extensions be serialised using tools.common.data_format?
420 for form_type, form in disco_infos.extensions.items(): 420 for form_type, form in list(disco_infos.extensions.items()):
421 fields = [] 421 fields = []
422 for field in form.fieldList: 422 for field in form.fieldList:
423 data = {"type": field.fieldType} 423 data = {"type": field.fieldType}
424 for attr in ("var", "label", "desc"): 424 for attr in ("var", "label", "desc"):
425 value = getattr(field, attr) 425 value = getattr(field, attr)
426 if value is not None: 426 if value is not None:
427 data[attr] = value 427 data[attr] = value
428 428
429 values = [field.value] if field.value is not None else field.values 429 values = [field.value] if field.value is not None else field.values
430 if field.fieldType == u"boolean": 430 if field.fieldType == "boolean":
431 values = [C.boolConst(v) for v in values] 431 values = [C.boolConst(v) for v in values]
432 fields.append((data, values)) 432 fields.append((data, values))
433 433
434 extensions[form_type or ""] = fields 434 extensions[form_type or ""] = fields
435 435
436 defer.returnValue(( 436 defer.returnValue((
437 disco_infos.features, 437 disco_infos.features,
438 [(cat, type_, name or "") 438 [(cat, type_, name or "")
439 for (cat, type_), name in disco_infos.identities.items()], 439 for (cat, type_), name in list(disco_infos.identities.items())],
440 extensions)) 440 extensions))
441 441
442 def items2tuples(self, disco_items): 442 def items2tuples(self, disco_items):
443 """convert disco items to tuple of strings 443 """convert disco items to tuple of strings
444 444
445 @param disco_items(iterable[disco.DiscoItem]): items 445 @param disco_items(iterable[disco.DiscoItem]): items
446 @return G(tuple[unicode,unicode,unicode]): serialised items 446 @return G(tuple[unicode,unicode,unicode]): serialised items
447 """ 447 """
448 for item in disco_items: 448 for item in disco_items:
449 if not item.entity: 449 if not item.entity:
450 log.warning(_(u"invalid item (no jid)")) 450 log.warning(_("invalid item (no jid)"))
451 continue 451 continue
452 yield (item.entity.full(), item.nodeIdentifier or "", item.name or "") 452 yield (item.entity.full(), item.nodeIdentifier or "", item.name or "")
453 453
454 @defer.inlineCallbacks 454 @defer.inlineCallbacks
455 def _discoItems( 455 def _discoItems(
456 self, entity_jid_s, node=u"", use_cache=True, profile_key=C.PROF_KEY_NONE 456 self, entity_jid_s, node="", use_cache=True, profile_key=C.PROF_KEY_NONE
457 ): 457 ):
458 """ Discovery method for the bridge 458 """ Discovery method for the bridge
459 459
460 @param entity_jid_s: entity we want to discover 460 @param entity_jid_s: entity we want to discover
461 @param node(unicode): optional node to use 461 @param node(unicode): optional node to use