comparison libervia/backend/memory/disco.py @ 4306:94e0968987cd

plugin XEP-0033: code modernisation, improve delivery, data validation: - Code has been rewritten using Pydantic models and `async` coroutines for data validation and cleaner element parsing/generation. - Delivery has been completely rewritten. It now works even if server doesn't support multicast, and send to local multicast service first. Delivering to local multicast service first is due to bad support of XEP-0033 in server (notably Prosody which has an incomplete implementation), and the current impossibility to detect if a sub-domain service handles fully multicast or only for local domains. This is a workaround to have a good balance between backward compatilibity and use of bandwith, and to make it work with the incoming email gateway implementation (the gateway will only deliver to entities of its own domain). - disco feature checking now uses `async` corountines. `host` implementation still use Deferred return values for compatibility with legacy code. rel 450
author Goffi <goffi@goffi.org>
date Thu, 26 Sep 2024 16:12:01 +0200
parents 0d7bb4df2343
children 554a87ae17a6
comparison
equal deleted inserted replaced
4305:4cd4922de876 4306:94e0968987cd
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 3
4 # SAT: a jabber client 4 # Libervia XMPP client
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org)
6 6
7 # This program is free software: you can redistribute it and/or modify 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 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 9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version. 10 # (at your option) any later version.
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 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/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from typing import Optional 20 from typing import Iterable, Optional, cast
21
22 from twisted.internet.interfaces import IReactorCore
21 from libervia.backend.core.i18n import _ 23 from libervia.backend.core.i18n import _
22 from libervia.backend.core import exceptions 24 from libervia.backend.core import exceptions
23 from libervia.backend.core.log import getLogger 25 from libervia.backend.core.log import getLogger
24 from libervia.backend.core.core_types import SatXMPPEntity 26 from libervia.backend.core.core_types import SatXMPPEntity
25 27
120 def load(self): 122 def load(self):
121 """Load persistent hashes""" 123 """Load persistent hashes"""
122 self.hashes = HashManager(persistent.PersistentDict("disco")) 124 self.hashes = HashManager(persistent.PersistentDict("disco"))
123 return self.hashes.load() 125 return self.hashes.load()
124 126
125 @defer.inlineCallbacks 127 async def has_feature(
126 def hasFeature(self, client, feature, jid_=None, node=""): 128 self,
129 client: SatXMPPEntity,
130 feature: str,
131 jid_: jid.JID | None = None,
132 node: str = "",
133 ) -> bool:
127 """Tell if an entity has the required feature 134 """Tell if an entity has the required feature
128 135
129 @param feature: feature namespace 136 @param feature: feature namespace
130 @param jid_: jid of the target, or None for profile's server 137 @param jid_: jid of the target, or None for profile's server
131 @param node(unicode): optional node to use for disco request 138 @param node(unicode): optional node to use for disco request
132 @return: a Deferred which fire a boolean (True if feature is available) 139 @return: a Deferred which fire a boolean (True if feature is available)
133 """ 140 """
134 disco_infos = yield self.get_infos(client, jid_, node) 141 disco_infos = await self.get_infos(client, jid_, node)
135 defer.returnValue(feature in disco_infos.features) 142 return feature in disco_infos.features
136 143
137 @defer.inlineCallbacks 144 async def check_feature(
138 def check_feature(self, client, feature, jid_=None, node=""): 145 self,
139 """Like hasFeature, but raise an exception is feature is not Found 146 client: SatXMPPEntity,
147 feature: str,
148 jid_: jid.JID | None = None,
149 node: str = "",
150 ) -> None:
151 """Like has_feature, but raise an exception is feature is not Found
140 152
141 @param feature: feature namespace 153 @param feature: feature namespace
142 @param jid_: jid of the target, or None for profile's server 154 @param jid_: jid of the target, or None for profile's server
143 @param node(unicode): optional node to use for disco request 155 @param node: optional node to use for disco request
144 156
145 @raise: exceptions.FeatureNotFound 157 @raise: exceptions.FeatureNotFound
146 """ 158 """
147 disco_infos = yield self.get_infos(client, jid_, node) 159 disco_infos = await self.get_infos(client, jid_, node)
148 if not feature in disco_infos.features: 160 if not feature in disco_infos.features:
149 raise failure.Failure(exceptions.FeatureNotFound()) 161 raise exceptions.FeatureNotFound()
150 162
151 @defer.inlineCallbacks 163 async def check_features(
152 def check_features(self, client, features, jid_=None, identity=None, node=""): 164 self,
165 client: SatXMPPEntity,
166 features: Iterable[str],
167 jid_: jid.JID | None = None,
168 identity: tuple[str, str] | None = None,
169 node: str = "",
170 ) -> None:
153 """Like check_feature, but check several features at once, and check also identity 171 """Like check_feature, but check several features at once, and check also identity
154 172
155 @param features(iterable[unicode]): features to check 173 @param features: features to check
156 @param jid_(jid.JID): jid of the target, or None for profile's server 174 @param jid_: jid of the target, or None for profile's server
157 @param node(unicode): optional node to use for disco request 175 @param node: optional node to use for disco request
158 @param identity(None, tuple(unicode, unicode): if not None, the entity must have an identity with this (category, type) tuple 176 @param identity: if not None, the entity must have an identity with this
177 (category, type) tuple
159 178
160 @raise: exceptions.FeatureNotFound 179 @raise: exceptions.FeatureNotFound
161 """ 180 """
162 disco_infos = yield self.get_infos(client, jid_, node) 181 disco_infos = await self.get_infos(client, jid_, node)
163 if not set(features).issubset(disco_infos.features): 182 if not set(features).issubset(disco_infos.features):
164 raise failure.Failure(exceptions.FeatureNotFound()) 183 raise exceptions.FeatureNotFound()
165 184
166 if identity is not None and identity not in disco_infos.identities: 185 if identity is not None and identity not in disco_infos.identities:
167 raise failure.Failure(exceptions.FeatureNotFound()) 186 raise exceptions.FeatureNotFound()
168 187
169 async def has_identity( 188 async def has_identity(
170 self, 189 self,
171 client: SatXMPPEntity, 190 client: SatXMPPEntity,
172 category: str, 191 category: str,
336 reactor.callLater( 355 reactor.callLater(
337 TIMEOUT, d.cancel 356 TIMEOUT, d.cancel
338 ) # FIXME: one bad service make a general timeout 357 ) # FIXME: one bad service make a general timeout
339 return d 358 return d
340 359
341 def find_features_set(self, client, features, identity=None, jid_=None): 360 async def find_features_set(
361 self,
362 client: SatXMPPEntity,
363 features: Iterable[str],
364 identity: tuple[str, str] | None = None,
365 jid_: jid.JID | None = None,
366 ) -> set[jid.JID]:
342 """Return entities (including jid_ and its items) offering features 367 """Return entities (including jid_ and its items) offering features
343 368
369 @param client: Client session.
344 @param features: iterable of features which must be present 370 @param features: iterable of features which must be present
345 @param identity(None, tuple(unicode, unicode)): if not None, accept only this 371 @param identity: if not None, accept only this (category/type) identity
346 (category/type) identity
347 @param jid_: the jid of the target server (None for profile's server) 372 @param jid_: the jid of the target server (None for profile's server)
348 @param profile: %(doc_profile)s
349 @return: a set of found entities 373 @return: a set of found entities
350 """ 374 """
351 if jid_ is None: 375 if jid_ is None:
352 jid_ = jid.JID(client.jid.host) 376 jid_ = jid.JID(client.jid.host)
353 features = set(features) 377 features = set(features)
354 found_entities = set() 378 found_entities = set()
355 379
356 def infos_cb(infos, entity): 380 def infos_cb(infos: disco.DiscoInfo, entity: jid.JID) -> None:
357 if entity is None: 381 if entity is None:
358 log.warning(_("received an item without jid")) 382 log.warning(_("Received an item without JID"))
359 return 383 return
360 if identity is not None and identity not in infos.identities: 384 if identity is not None and identity not in infos.identities:
361 return 385 return
362 if features.issubset(infos.features): 386 if features.issubset(infos.features):
363 found_entities.add(entity) 387 found_entities.add(entity)
364 388
365 def got_items(items): 389 items = await self.get_items(client, jid_)
366 defer_list = [] 390 defer_list = []
367 for entity in [jid_] + [item.entity for item in items]: 391 for entity in [jid_] + [item.entity for item in items]:
368 infos_d = self.get_infos(client, entity) 392 infos_d = self.get_infos(client, entity)
369 infos_d.addCallbacks(infos_cb, self._infos_eb, [entity], None, [entity]) 393 infos_d.addCallbacks(infos_cb, self._infos_eb, [entity], None, [entity])
370 defer_list.append(infos_d) 394 infos_d.addTimeout(TIMEOUT, cast(IReactorCore, reactor))
371 return defer.DeferredList(defer_list) 395 defer_list.append(infos_d)
372 396 await defer.DeferredList(defer_list)
373 d = self.get_items(client, jid_) 397 return found_entities
374 d.addCallback(got_items)
375 d.addCallback(lambda __: found_entities)
376 reactor.callLater(
377 TIMEOUT, d.cancel
378 ) # FIXME: one bad service make a general timeout
379 return d
380 398
381 def generate_hash(self, services): 399 def generate_hash(self, services):
382 """Generate a unique hash for given service 400 """Generate a unique hash for given service
383 401
384 hash algorithm is the one described in XEP-0115 402 hash algorithm is the one described in XEP-0115